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 234a96b2af17c..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. @@ -47,6 +48,21 @@ export function validateTextareaNonEmpty(textarea: HTMLTextAreaElement) { return true; } +/** Returns whether the user currently has the monospace font setting enabled */ +function isMonospaceEnabled() { + return getLocalStorageSetting('markdown-editor-monospace') === 'true'; +} + +/** Apply font to the provided or all textareas on the page and optionally save on localStorage */ +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', enabled); + } + if (save) { + setLocalStorageSetting('markdown-editor-monospace', String(enabled)); + } +} + type Heights = { minHeight?: string, height?: string, @@ -141,20 +157,25 @@ export class ComboMarkdownEditor { } const monospaceButton = this.container.querySelector('.markdown-switch-monospace')!; - const monospaceEnabled = localStorage?.getItem('markdown-editor-monospace') === 'true'; + const monospaceEnabled = isMonospaceEnabled(); + 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 = localStorage?.getItem('markdown-editor-monospace') !== 'true'; - localStorage.setItem('markdown-editor-monospace', String(enabled)); - this.textarea.classList.toggle('tw-font-mono', enabled); + const enabled = !isMonospaceEnabled(); + 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 when it was changed in another tab + addLocalStorageChangeListener('markdown-editor-monospace', () => { + applyMonospace(isMonospaceEnabled(), {save: false}); + }); + if (this.supportEasyMDE) { const easymdeButton = this.container.querySelector('.markdown-switch-easymde')!; easymdeButton.addEventListener('click', async (e) => { @@ -403,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..a5f2dc0e4b2bb --- /dev/null +++ b/web_src/js/modules/storage.ts @@ -0,0 +1,18 @@ +/** 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) { + window.addEventListener('storage', (e: StorageEvent) => { + if (e.storageArea === localStorage && e.key === key) { + listener(e); + } + }); +}