diff --git a/src/static/js/broadcast.ts b/src/static/js/broadcast.ts index 8551f1d0c..3e25ad6e3 100644 --- a/src/static/js/broadcast.ts +++ b/src/static/js/broadcast.ts @@ -565,7 +565,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro const bgcolor = typeof data.colorId === 'number' ? clientVars.colorPalette[data.colorId] : data.colorId; if (bgcolor) { - const selector = dynamicCSS.selectorStyle(`.${linestylefilter.getAuthorClassName(author)}`); + const selector = dynamicCSS.selectorStyle(`.authorColors .${linestylefilter.getAuthorClassName(author)}`); selector.backgroundColor = bgcolor; selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5) ? '#ffffff' : '#000000'; // see ace2_inner.js for the other part diff --git a/src/static/js/pad_mode.ts b/src/static/js/pad_mode.ts index b49029628..a1622ea02 100644 --- a/src/static/js/pad_mode.ts +++ b/src/static/js/pad_mode.ts @@ -64,6 +64,9 @@ class PadModeController { private chatHeaderEl: HTMLElement | null = null; private playbackChangeListener: ((e: Event) => void) | null = null; private followChangeListener: ((e: Event) => void) | null = null; + private authorColorsListeners: Array<{el: HTMLElement; type: string; fn: EventListener}> = []; + private fontFamilyListeners: Array<{el: HTMLElement; type: string; fn: EventListener}> = []; + private lineNumbersListeners: Array<{el: HTMLElement; type: string; fn: EventListener}> = []; // Outer history controls (#history-controls) — bridge listeners. private outerControlListeners: Array<{el: HTMLElement; type: string; fn: EventListener}> = []; @@ -229,6 +232,12 @@ class PadModeController { // is destroyed on exit so any callbacks die with it. this.outerControlListeners.forEach(({el, type, fn}) => el.removeEventListener(type, fn)); this.outerControlListeners = []; + this.authorColorsListeners.forEach(({el, type, fn}) => el.removeEventListener(type, fn)); + this.authorColorsListeners = []; + this.fontFamilyListeners.forEach(({el, type, fn}) => el.removeEventListener(type, fn)); + this.fontFamilyListeners = []; + this.lineNumbersListeners.forEach(({el, type, fn}) => el.removeEventListener(type, fn)); + this.lineNumbersListeners = []; } private mountIframe(rev: number | null): void { @@ -532,6 +541,39 @@ class PadModeController { }; followCb.addEventListener('change', this.followChangeListener); } + + const bridgeAuthorColors = (cb: HTMLInputElement | null) => { + if (!cb) return; + const listener = () => { + try { inner.BroadcastSlider?.setShowAuthorColors?.(cb.checked); } catch (_e) {} + }; + cb.addEventListener('change', listener); + this.authorColorsListeners.push({el: cb, type: 'change', fn: listener}); + }; + bridgeAuthorColors(document.getElementById('options-colorscheck') as HTMLInputElement | null); + bridgeAuthorColors(document.getElementById('padsettings-options-colorscheck') as HTMLInputElement | null); + + const bridgePadFontFamily = (sel: HTMLSelectElement | null) => { + if (!sel) return; + const listener = () => { + try { inner.BroadcastSlider?.setPadFontFamily?.(sel.value); } catch (_e) {} + }; + sel.addEventListener('change', listener); + this.fontFamilyListeners.push({el: sel, type: 'change', fn: listener}); + }; + bridgePadFontFamily(document.getElementById('viewfontmenu') as HTMLSelectElement | null); + bridgePadFontFamily(document.getElementById('padsettings-viewfontmenu') as HTMLSelectElement | null); + + const bridgeLineNumbers = (cb: HTMLInputElement | null) => { + if (!cb) return; + const listener = () => { + try { inner.BroadcastSlider?.setShowLineNumbers?.(cb.checked); } catch (_e) {} + }; + cb.addEventListener('change', listener); + this.lineNumbersListeners.push({el: cb, type: 'change', fn: listener}); + }; + bridgeLineNumbers(document.getElementById('options-linenoscheck') as HTMLInputElement | null); + bridgeLineNumbers(document.getElementById('padsettings-options-linenoscheck') as HTMLInputElement | null); } private setInnerRevision(rev: number): void { diff --git a/src/static/js/timeslider.ts b/src/static/js/timeslider.ts index dacc93b93..35f0b5cfd 100644 --- a/src/static/js/timeslider.ts +++ b/src/static/js/timeslider.ts @@ -67,6 +67,11 @@ const applyShowLineNumbers = (showLineNumbers) => { window.requestAnimationFrame(() => $(window).trigger('resize')); }; +const applyShowAuthorColors = (showAuthorColors) => { + $('#innerdocbody').toggleClass('authorColors', showAuthorColors); + $('#sidedivinner').toggleClass('authorColors', showAuthorColors); +}; + const init = () => { padutils.setupGlobalExceptionHandler(); $(document).ready(() => { @@ -193,6 +198,14 @@ const handleClientVars = (message) => { // load all script that doesn't work without the clientVars BroadcastSlider = require('./broadcast_slider') .loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); + BroadcastSlider.setShowAuthorColors = (showAuthorColors) => { + applyShowAuthorColors(showAuthorColors); + setPadPref('showAuthorshipColors', showAuthorColors); + }; + BroadcastSlider.setShowLineNumbers = (showLineNumbers) => { + applyShowLineNumbers(showLineNumbers); + setPadPref('showLineNumbers', showLineNumbers); + }; // Exposed on window so the outer pad shell (issue #7659 in-place history // mode) can subscribe to slider movement without postMessage round-trips. (window as any).BroadcastSlider = BroadcastSlider; @@ -240,11 +253,31 @@ const handleClientVars = (message) => { }); applyShowLineNumbers(readPadPrefs().showLineNumbers !== false); - // font family change + // Read authorship colors preference from cookie (set by pad editor) + applyShowAuthorColors(readPadPrefs().showAuthorshipColors !== false); + + // font family + const applyPadFontFamily = (fontFamily) => { + if (fontFamily) { + $('#innerdocbody').css('font-family', fontFamily); + } else { + $('#innerdocbody').css('font-family', ''); + } + }; + const padFontFamily = readPadPrefs().padFontFamily; + if (padFontFamily) $('#viewfontmenu').val(padFontFamily); + applyPadFontFamily(padFontFamily); $('#viewfontmenu').on('change', function () { - $('#innerdocbody').css('font-family', $(this).val() || ''); + const fontFamily = $(this).val() || ''; + setPadPref('padFontFamily', fontFamily); + applyPadFontFamily(fontFamily); }); + BroadcastSlider.setPadFontFamily = (fontFamily) => { + applyPadFontFamily(fontFamily); + setPadPref('padFontFamily', fontFamily); + }; + const savedPlaybackSpeed = Cookies.get(`${cp}${playbackSpeedCookie}`) || '100'; $('#playbackspeed').val(savedPlaybackSpeed); BroadcastSlider.setPlaybackSpeed(savedPlaybackSpeed); diff --git a/src/static/js/vendors/nice-select.ts b/src/static/js/vendors/nice-select.ts index 4f1ff8ab1..0d2b9a03e 100644 --- a/src/static/js/vendors/nice-select.ts +++ b/src/static/js/vendors/nice-select.ts @@ -165,7 +165,11 @@ var text = $option.data('display') || $option.text(); $dropdown.find('.current').text(text); - $dropdown.prev('select').val($option.data('value')).trigger('change'); + const $nativeSelect = $dropdown.prev('select'); + $nativeSelect.val($option.data('value')).trigger('change'); + // Fire native event for handlers attached via addEventListener (e.g. + // the pad_mode.ts settings bridge to the embedded timeslider iframe). + $nativeSelect[0]?.dispatchEvent(new Event('change', {bubbles: true})); }); // Keyboard events diff --git a/src/tests/frontend-new/specs/timeslider_author_colors.spec.ts b/src/tests/frontend-new/specs/timeslider_author_colors.spec.ts new file mode 100644 index 000000000..9e1fe250c --- /dev/null +++ b/src/tests/frontend-new/specs/timeslider_author_colors.spec.ts @@ -0,0 +1,84 @@ +import {expect, test} from "@playwright/test"; +import {clearPadContent, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.describe('timeslider authorship colors', function () { + test.beforeEach(async ({context}) => { + await context.clearCookies(); + }); + + test('respects showAuthorshipColors=false cookie from pad editor', async function ({page}) { + const padId = await goToNewPad(page); + await clearPadContent(page); + await writeToPad(page, 'Hello from author one'); + + await page.context().addCookies([{ + name: 'prefsHttp', + value: encodeURIComponent(JSON.stringify({showAuthorshipColors: false})), + url: 'http://localhost:9001', + }]); + + await page.goto(`http://localhost:9001/p/${padId}/timeslider?embed=1`); + await page.waitForSelector('#timeslider-wrapper', {state: 'visible'}); + await page.waitForTimeout(500); + + await expect(page.locator('#innerdocbody')).not.toHaveClass(/authorColors/); + }); + + test('shows author colors by default (cookie unset)', async function ({page}) { + const padId = await goToNewPad(page); + await clearPadContent(page); + await writeToPad(page, 'Hello from author one'); + + await page.goto(`http://localhost:9001/p/${padId}/timeslider?embed=1`); + await page.waitForSelector('#timeslider-wrapper', {state: 'visible'}); + await page.waitForTimeout(500); + + await expect(page.locator('#innerdocbody')).toHaveClass(/authorColors/); + }); + + test('font type selector applies font-family to innerdocbody', async function ({page}) { + const padId = await goToNewPad(page); + await clearPadContent(page); + await writeToPad(page, 'Test content'); + + await page.goto(`http://localhost:9001/p/${padId}/timeslider?embed=1`); + await page.waitForSelector('#timeslider-wrapper', {state: 'visible'}); + await page.waitForTimeout(500); + + // Use evaluate() to trigger font change via jQuery, bypassing the + // nice-select UI and settings-popup open/close lifecycle. + await page.evaluate(() => { + const el = document.getElementById('viewfontmenu') as HTMLSelectElement; + if (el) { + el.value = 'RobotoMono'; + el.dispatchEvent(new Event('change', {bubbles: true})); + } + }); + await page.waitForTimeout(200); + + const fontFamily = await page.locator('#innerdocbody').evaluate( + (el) => getComputedStyle(el).fontFamily); + expect(fontFamily).toContain('RobotoMono'); + }); + + test('font-type selection persists and restores from cookie', async function ({page, context}) { + const padId = await goToNewPad(page); + await clearPadContent(page); + await writeToPad(page, 'Test content'); + + // Set font cookie before loading timeslider + await context.addCookies([{ + name: 'prefsHttp', + value: encodeURIComponent(JSON.stringify({padFontFamily: 'Alegreya'})), + url: 'http://localhost:9001', + }]); + + await page.goto(`http://localhost:9001/p/${padId}/timeslider?embed=1`); + await page.waitForSelector('#timeslider-wrapper', {state: 'visible'}); + await page.waitForTimeout(500); + + const fontFamily = await page.locator('#innerdocbody').evaluate( + (el) => getComputedStyle(el).fontFamily); + expect(fontFamily).toContain('Alegreya'); + }); +});