From 4f9c25be314e9eb52b1b034dbbad78384c78b7d4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 17 Dec 2025 20:20:24 +0100 Subject: [PATCH 1/8] Fix comment editor monospace toggle for edit mode --- .../js/features/comp/ComboMarkdownEditor.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 234a96b2af17c..b250773c5046b 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -47,6 +47,19 @@ export function validateTextareaNonEmpty(textarea: HTMLTextAreaElement) { return true; } +/** Returns whether the user currently has the monospace font setting enabled */ +function isMonospaceEnabled() { + return localStorage?.getItem('markdown-editor-monospace') === 'true'; +} + +/** Apply font variant to the provided or all textareas on the page */ +function applyFontVariant(monospaceEnabled: boolean, textarea?: HTMLTextAreaElement) { + for (const el of textarea ? [textarea] : document.querySelectorAll('.markdown-text-editor')) { + el.classList.toggle('tw-font-mono', monospaceEnabled); + } + localStorage.setItem('markdown-editor-monospace', String(monospaceEnabled)); +} + type Heights = { minHeight?: string, height?: string, @@ -141,15 +154,15 @@ export class ComboMarkdownEditor { } const monospaceButton = this.container.querySelector('.markdown-switch-monospace')!; - const monospaceEnabled = localStorage?.getItem('markdown-editor-monospace') === 'true'; + const monospaceEnabled = isMonospaceEnabled(); + applyFontVariant(monospaceEnabled, this.textarea); const monospaceText = monospaceButton.getAttribute(monospaceEnabled ? 'data-disable-text' : 'data-enable-text')!; monospaceButton.setAttribute('data-tooltip-content', monospaceText); monospaceButton.setAttribute('aria-checked', String(monospaceEnabled)); monospaceButton.addEventListener('click', (e) => { e.preventDefault(); - const enabled = localStorage?.getItem('markdown-editor-monospace') !== 'true'; - localStorage.setItem('markdown-editor-monospace', String(enabled)); - this.textarea.classList.toggle('tw-font-mono', enabled); + const enabled = !isMonospaceEnabled(); + applyFontVariant(enabled); const text = monospaceButton.getAttribute(enabled ? 'data-disable-text' : 'data-enable-text')!; monospaceButton.setAttribute('data-tooltip-content', text); monospaceButton.setAttribute('aria-checked', String(enabled)); From 8fcfbb752a01e4e631c9bf2e3822f1d09a85383b Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 17 Dec 2025 20:23:15 +0100 Subject: [PATCH 2/8] rename --- web_src/js/features/comp/ComboMarkdownEditor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index b250773c5046b..e7b3a6ceadf07 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -53,7 +53,7 @@ function isMonospaceEnabled() { } /** Apply font variant to the provided or all textareas on the page */ -function applyFontVariant(monospaceEnabled: boolean, textarea?: HTMLTextAreaElement) { +function applyFont(monospaceEnabled: boolean, textarea?: HTMLTextAreaElement) { for (const el of textarea ? [textarea] : document.querySelectorAll('.markdown-text-editor')) { el.classList.toggle('tw-font-mono', monospaceEnabled); } @@ -155,14 +155,14 @@ export class ComboMarkdownEditor { const monospaceButton = this.container.querySelector('.markdown-switch-monospace')!; const monospaceEnabled = isMonospaceEnabled(); - applyFontVariant(monospaceEnabled, this.textarea); + applyFont(monospaceEnabled, this.textarea); const monospaceText = monospaceButton.getAttribute(monospaceEnabled ? 'data-disable-text' : 'data-enable-text')!; monospaceButton.setAttribute('data-tooltip-content', monospaceText); monospaceButton.setAttribute('aria-checked', String(monospaceEnabled)); monospaceButton.addEventListener('click', (e) => { e.preventDefault(); const enabled = !isMonospaceEnabled(); - applyFontVariant(enabled); + applyFont(enabled); const text = monospaceButton.getAttribute(enabled ? 'data-disable-text' : 'data-enable-text')!; monospaceButton.setAttribute('data-tooltip-content', text); monospaceButton.setAttribute('aria-checked', String(enabled)); From 439bf5a21a29c0cd78fc4160bd389d9fed111355 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 17 Dec 2025 20:26:57 +0100 Subject: [PATCH 3/8] update comment --- web_src/js/features/comp/ComboMarkdownEditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index e7b3a6ceadf07..7deb09498415f 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -52,7 +52,7 @@ function isMonospaceEnabled() { return localStorage?.getItem('markdown-editor-monospace') === 'true'; } -/** Apply font variant to the provided or all textareas on the page */ +/** Apply font to the provided or all textareas on the page */ function applyFont(monospaceEnabled: boolean, textarea?: HTMLTextAreaElement) { for (const el of textarea ? [textarea] : document.querySelectorAll('.markdown-text-editor')) { el.classList.toggle('tw-font-mono', monospaceEnabled); From f45185dcae4b38549b1ad34bfd3d23e027a7fba3 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 17 Dec 2025 20:59:54 +0100 Subject: [PATCH 4/8] add storage event handling --- .../js/features/comp/ComboMarkdownEditor.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 7deb09498415f..4cc24ca326cf7 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -52,12 +52,14 @@ function isMonospaceEnabled() { return localStorage?.getItem('markdown-editor-monospace') === 'true'; } -/** Apply font to the provided or all textareas on the page */ -function applyFont(monospaceEnabled: boolean, textarea?: HTMLTextAreaElement) { +/** Apply font to the provided or all textareas on the page and optionally save on localStorage */ +function applyMonospace(monospaceEnabled: boolean, {textarea, save}: {textarea?: HTMLTextAreaElement, save?: boolean}) { for (const el of textarea ? [textarea] : document.querySelectorAll('.markdown-text-editor')) { el.classList.toggle('tw-font-mono', monospaceEnabled); } - localStorage.setItem('markdown-editor-monospace', String(monospaceEnabled)); + if (save) { + localStorage.setItem('markdown-editor-monospace', String(monospaceEnabled)); + } } type Heights = { @@ -155,19 +157,26 @@ export class ComboMarkdownEditor { const monospaceButton = this.container.querySelector('.markdown-switch-monospace')!; const monospaceEnabled = isMonospaceEnabled(); - applyFont(monospaceEnabled, this.textarea); + applyMonospace(monospaceEnabled, {textarea: this.textarea, save: false}); const monospaceText = monospaceButton.getAttribute(monospaceEnabled ? 'data-disable-text' : 'data-enable-text')!; monospaceButton.setAttribute('data-tooltip-content', monospaceText); monospaceButton.setAttribute('aria-checked', String(monospaceEnabled)); monospaceButton.addEventListener('click', (e) => { e.preventDefault(); const enabled = !isMonospaceEnabled(); - applyFont(enabled); + applyMonospace(enabled, {save: true}); const text = monospaceButton.getAttribute(enabled ? 'data-disable-text' : 'data-enable-text')!; monospaceButton.setAttribute('data-tooltip-content', text); monospaceButton.setAttribute('aria-checked', String(enabled)); }); + // apply setting was changed in another tab + window.addEventListener('storage', (e) => { + if (e.key === 'markdown-editor-monospace') { + applyMonospace(isMonospaceEnabled(), {save: false}); + } + }); + if (this.supportEasyMDE) { const easymdeButton = this.container.querySelector('.markdown-switch-easymde')!; easymdeButton.addEventListener('click', async (e) => { From f29f63a39073707342f9a34cd850dce628819957 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 17 Dec 2025 21:03:46 +0100 Subject: [PATCH 5/8] rename var --- web_src/js/features/comp/ComboMarkdownEditor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 4cc24ca326cf7..0aadd1bce452e 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -53,12 +53,12 @@ function isMonospaceEnabled() { } /** Apply font to the provided or all textareas on the page and optionally save on localStorage */ -function applyMonospace(monospaceEnabled: boolean, {textarea, save}: {textarea?: HTMLTextAreaElement, save?: boolean}) { +function applyMonospace(enabled: boolean, {textarea, save}: {textarea?: HTMLTextAreaElement, save?: boolean}) { for (const el of textarea ? [textarea] : document.querySelectorAll('.markdown-text-editor')) { - el.classList.toggle('tw-font-mono', monospaceEnabled); + el.classList.toggle('tw-font-mono', enabled); } if (save) { - localStorage.setItem('markdown-editor-monospace', String(monospaceEnabled)); + localStorage.setItem('markdown-editor-monospace', String(enabled)); } } From 9f13c21543973310232422cfe2dfee41fa3097bf Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 17 Dec 2025 21:06:50 +0100 Subject: [PATCH 6/8] verify e.storageArea --- web_src/js/features/comp/ComboMarkdownEditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 0aadd1bce452e..b643137ff17b3 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -172,7 +172,7 @@ export class ComboMarkdownEditor { // apply setting was changed in another tab window.addEventListener('storage', (e) => { - if (e.key === 'markdown-editor-monospace') { + if (e.storageArea === localStorage && e.key === 'markdown-editor-monospace') { applyMonospace(isMonospaceEnabled(), {save: false}); } }); From f10ab8d9b0d19fb6956ab3c6efcc0781b48bde08 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 17 Dec 2025 21:36:56 +0100 Subject: [PATCH 7/8] add minimal localStorage wrappers --- web_src/js/components/DiffFileTree.vue | 5 +++-- web_src/js/components/RepoActionView.vue | 5 +++-- web_src/js/features/citation.ts | 7 ++++--- .../js/features/comp/ComboMarkdownEditor.ts | 17 ++++++++-------- web_src/js/features/repo-common.ts | 9 +++++---- web_src/js/modules/storage.ts | 20 +++++++++++++++++++ 6 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 web_src/js/modules/storage.ts diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue index e2934b967eb0e..010f05100b190 100644 --- a/web_src/js/components/DiffFileTree.vue +++ b/web_src/js/components/DiffFileTree.vue @@ -4,6 +4,7 @@ import {toggleElem} from '../utils/dom.ts'; import {diffTreeStore} from '../modules/diff-file.ts'; import {setFileFolding} from '../features/file-fold.ts'; import {onMounted, onUnmounted} from 'vue'; +import {getLocalStorageSetting, setLocalStorageSetting} from '../modules/storage.ts'; const LOCAL_STORAGE_KEY = 'diff_file_tree_visible'; @@ -11,7 +12,7 @@ const store = diffTreeStore(); onMounted(() => { // Default to true if unset - store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false'; + store.fileTreeIsVisible = getLocalStorageSetting(LOCAL_STORAGE_KEY) !== 'false'; document.querySelector('.diff-toggle-file-tree-button')!.addEventListener('click', toggleVisibility); hashChangeListener(); @@ -43,7 +44,7 @@ function toggleVisibility() { function updateVisibility(visible: boolean) { store.fileTreeIsVisible = visible; - localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible.toString()); + setLocalStorageSetting(LOCAL_STORAGE_KEY, store.fileTreeIsVisible.toString()); updateState(store.fileTreeIsVisible); } diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 357a2ba10eefa..1cf4ae968917f 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -8,6 +8,7 @@ import {renderAnsi} from '../render/ansi.ts'; import {POST, DELETE} from '../modules/fetch.ts'; import type {IntervalId} from '../types.ts'; import {toggleFullScreen} from '../utils.ts'; +import {getLocalStorageSetting, setLocalStorageSetting} from '../modules/storage.ts'; // see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts" type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; @@ -73,7 +74,7 @@ type LocaleStorageOptions = { function getLocaleStorageOptions(): LocaleStorageOptions { try { - const optsJson = localStorage.getItem('actions-view-options'); + const optsJson = getLocalStorageSetting('actions-view-options'); if (optsJson) return JSON.parse(optsJson); } catch {} // if no options in localStorage, or failed to parse, return default options @@ -224,7 +225,7 @@ export default defineComponent({ methods: { saveLocaleStorageOptions() { const opts: LocaleStorageOptions = {autoScroll: this.optionAlwaysAutoScroll, expandRunning: this.optionAlwaysExpandRunning}; - localStorage.setItem('actions-view-options', JSON.stringify(opts)); + setLocalStorageSetting('actions-view-options', JSON.stringify(opts)); }, // get the job step logs container ('.job-step-logs') diff --git a/web_src/js/features/citation.ts b/web_src/js/features/citation.ts index d5ecb52e72a7f..509bb75505ea6 100644 --- a/web_src/js/features/citation.ts +++ b/web_src/js/features/citation.ts @@ -1,5 +1,6 @@ import {getCurrentLocale} from '../utils.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; +import {getLocalStorageSetting, setLocalStorageSetting} from '../modules/storage.ts'; const {pageData} = window.config; @@ -38,7 +39,7 @@ export async function initCitationFileCopyContent() { if ((!citationCopyApa && !citationCopyBibtex) || !inputContent) return; const updateUi = () => { - const isBibtex = (localStorage.getItem('citation-copy-format') || defaultCitationFormat) === 'bibtex'; + const isBibtex = (getLocalStorageSetting('citation-copy-format') || defaultCitationFormat) === 'bibtex'; const copyContent = (isBibtex ? citationCopyBibtex : citationCopyApa).getAttribute('data-text')!; inputContent.value = copyContent; citationCopyBibtex.classList.toggle('primary', isBibtex); @@ -55,12 +56,12 @@ export async function initCitationFileCopyContent() { updateUi(); citationCopyApa.addEventListener('click', () => { - localStorage.setItem('citation-copy-format', 'apa'); + setLocalStorageSetting('citation-copy-format', 'apa'); updateUi(); }); citationCopyBibtex.addEventListener('click', () => { - localStorage.setItem('citation-copy-format', 'bibtex'); + setLocalStorageSetting('citation-copy-format', 'bibtex'); updateUi(); }); diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index b643137ff17b3..4791450e8d697 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -24,6 +24,7 @@ import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts'; import {createTippy} from '../../modules/tippy.ts'; import {fomanticQuery} from '../../modules/fomantic/base.ts'; import type EasyMDE from 'easymde'; +import {addLocalStorageChangeListener, getLocalStorageSetting, setLocalStorageSetting} from '../../modules/storage.ts'; /** * validate if the given textarea is non-empty. @@ -49,7 +50,7 @@ export function validateTextareaNonEmpty(textarea: HTMLTextAreaElement) { /** Returns whether the user currently has the monospace font setting enabled */ function isMonospaceEnabled() { - return localStorage?.getItem('markdown-editor-monospace') === 'true'; + return getLocalStorageSetting('markdown-editor-monospace') === 'true'; } /** Apply font to the provided or all textareas on the page and optionally save on localStorage */ @@ -58,7 +59,7 @@ function applyMonospace(enabled: boolean, {textarea, save}: {textarea?: HTMLText el.classList.toggle('tw-font-mono', enabled); } if (save) { - localStorage.setItem('markdown-editor-monospace', String(enabled)); + setLocalStorageSetting('markdown-editor-monospace', String(enabled)); } } @@ -170,11 +171,9 @@ export class ComboMarkdownEditor { monospaceButton.setAttribute('aria-checked', String(enabled)); }); - // apply setting was changed in another tab - window.addEventListener('storage', (e) => { - if (e.storageArea === localStorage && e.key === 'markdown-editor-monospace') { - applyMonospace(isMonospaceEnabled(), {save: false}); - } + // apply setting when it was changed in another tab + addLocalStorageChangeListener('markdown-editor-monospace', () => { + applyMonospace(isMonospaceEnabled(), {save: false}); }); if (this.supportEasyMDE) { @@ -425,10 +424,10 @@ export class ComboMarkdownEditor { } get userPreferredEditor(): string { - return window.localStorage.getItem(`markdown-editor-${this.previewMode ?? 'default'}`) || ''; + return getLocalStorageSetting(`markdown-editor-${this.previewMode ?? 'default'}`) || ''; } set userPreferredEditor(s: string) { - window.localStorage.setItem(`markdown-editor-${this.previewMode ?? 'default'}`, s); + setLocalStorageSetting(`markdown-editor-${this.previewMode ?? 'default'}`, s); } } diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index ac753805d326c..98f76588cddfd 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -6,6 +6,7 @@ import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue'; import {createApp} from 'vue'; import {toOriginUrl} from '../utils/url.ts'; import {createTippy} from '../modules/tippy.ts'; +import {getLocalStorageSetting, setLocalStorageSetting} from '../modules/storage.ts'; async function onDownloadArchive(e: Event) { e.preventDefault(); @@ -57,7 +58,7 @@ function initCloneSchemeUrlSelection(parent: Element) { const tabSsh = parent.querySelector('.repo-clone-ssh'); const tabTea = parent.querySelector('.repo-clone-tea'); const updateClonePanelUi = function() { - let scheme = localStorage.getItem('repo-clone-protocol')!; + let scheme = getLocalStorageSetting('repo-clone-protocol')!; if (!['https', 'ssh', 'tea'].includes(scheme)) { scheme = 'https'; } @@ -114,15 +115,15 @@ function initCloneSchemeUrlSelection(parent: Element) { updateClonePanelUi(); // tabSsh or tabHttps might not both exist, eg: guest view, or one is disabled by the server tabHttps?.addEventListener('click', () => { - localStorage.setItem('repo-clone-protocol', 'https'); + setLocalStorageSetting('repo-clone-protocol', 'https'); updateClonePanelUi(); }); tabSsh?.addEventListener('click', () => { - localStorage.setItem('repo-clone-protocol', 'ssh'); + setLocalStorageSetting('repo-clone-protocol', 'ssh'); updateClonePanelUi(); }); tabTea?.addEventListener('click', () => { - localStorage.setItem('repo-clone-protocol', 'tea'); + setLocalStorageSetting('repo-clone-protocol', 'tea'); updateClonePanelUi(); }); elCloneUrlInput.addEventListener('focus', () => { diff --git a/web_src/js/modules/storage.ts b/web_src/js/modules/storage.ts new file mode 100644 index 0000000000000..9f035087ca507 --- /dev/null +++ b/web_src/js/modules/storage.ts @@ -0,0 +1,20 @@ +/** Get a setting from localStorage */ +export function getLocalStorageSetting(key: string) { + return localStorage?.getItem(key); +} + +/** Set a setting in localStorage */ +export function setLocalStorageSetting(key: string, value: string) { + return localStorage?.setItem(key, value); +} + +/** Add a listener to the 'storage' event for given setting key. This event only fires in non-current tabs. */ +export function addLocalStorageChangeListener(key: string, listener: (e: StorageEvent) => void) { + const fn = (e: StorageEvent) => { + if (e.storageArea === localStorage && e.key === key) { + listener(e); + } + }; + window.addEventListener('storage', fn); + return fn; // for unregister +} From f30f4a002e80c0bcada1889e9c39c08f38da9c84 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 17 Dec 2025 21:41:15 +0100 Subject: [PATCH 8/8] remove return value, not yet needed --- web_src/js/modules/storage.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web_src/js/modules/storage.ts b/web_src/js/modules/storage.ts index 9f035087ca507..a5f2dc0e4b2bb 100644 --- a/web_src/js/modules/storage.ts +++ b/web_src/js/modules/storage.ts @@ -10,11 +10,9 @@ export function setLocalStorageSetting(key: string, value: string) { /** Add a listener to the 'storage' event for given setting key. This event only fires in non-current tabs. */ export function addLocalStorageChangeListener(key: string, listener: (e: StorageEvent) => void) { - const fn = (e: StorageEvent) => { + window.addEventListener('storage', (e: StorageEvent) => { if (e.storageArea === localStorage && e.key === key) { listener(e); } - }; - window.addEventListener('storage', fn); - return fn; // for unregister + }); }