From 226487b0091f38402b653784f35fb4e24f0a2be1 Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 5 Jun 2026 12:37:26 +0100 Subject: [PATCH 1/2] timeslider: respect author colors, font, line numbers from pad editor - nice-select.ts: dispatch native change event after jQuery trigger so addEventListener-based bridges (pad_mode.ts) fire (jQuery 3.7.1 trigger() does not dispatch native DOM events) - timeslider.ts: fix font-family reset (jQuery 3 ignores null css value); add showAuthorColors + showLineNumbers + padFontFamily support; wire cookie read/write for all three settings - broadcast.ts: gate author color CSS on .authorColors class - pad_mode.ts: bridge author colors, font family, line numbers checkboxes from pad settings into embedded timeslider iframe - add Playwright test for showAuthorshipColors --- src/static/js/broadcast.ts | 2 +- src/static/js/pad_mode.ts | 42 ++++++++++ src/static/js/timeslider.ts | 37 +++++++- src/static/js/vendors/nice-select.ts | 6 +- .../specs/timeslider_author_colors.spec.ts | 84 +++++++++++++++++++ 5 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 src/tests/frontend-new/specs/timeslider_author_colors.spec.ts diff --git a/src/static/js/broadcast.ts b/src/static/js/broadcast.ts index 8551f1d0cfa..3e25ad6e36f 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 b4902962826..a1622ea029b 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 dacc93b934b..35f0b5cfd43 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 4f1ff8ab16d..0d2b9a03e57 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 00000000000..9e1fe250c87 --- /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'); + }); +}); From 5e039a8ad9c354870e0c9267f1c1d72c25ef88e0 Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 5 Jun 2026 12:49:37 +0100 Subject: [PATCH 2/2] timeslider: tidy settings-bridge wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor only — no behaviour change. pad_mode.ts: collapse the five ad-hoc listener stores (playbackChange- Listener, followChangeListener, authorColorsListeners, fontFamily- Listeners, lineNumbersListeners) into the single outerControlListeners list via a shared bindOuter() helper, so every outer-control listener is registered and torn down through one uniform path. The three view-setting bridges (author colours, font family, line numbers) become one data-driven bridgeView() over their element ids instead of three near-identical closures. timeslider.ts: hoist applyPadFontFamily to a top-level helper next to applyShowAuthorColors (keeping the '' reset for jQuery 3), and co-locate the three BroadcastSlider view-setting methods that were split across the function. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/static/js/pad_mode.ts | 111 ++++++++++++------------------------ src/static/js/timeslider.ts | 36 ++++++------ 2 files changed, 56 insertions(+), 91 deletions(-) diff --git a/src/static/js/pad_mode.ts b/src/static/js/pad_mode.ts index a1622ea029b..499d9832b80 100644 --- a/src/static/js/pad_mode.ts +++ b/src/static/js/pad_mode.ts @@ -62,12 +62,8 @@ class PadModeController { private usersSnapshot: string | null = null; private chatHeaderSnapshot: {parent: HTMLElement; sibling: Node | null} | null = null; 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. + // Every listener we attach to an outer Settings / history control is + // tracked here so teardownBridges() can remove them all in one pass. private outerControlListeners: Array<{el: HTMLElement; type: string; fn: EventListener}> = []; constructor() { @@ -218,26 +214,20 @@ class PadModeController { this.exportSnapshot.forEach((href, anchor) => { anchor.setAttribute('href', href); }); this.exportSnapshot = null; } - if (this.playbackChangeListener) { - const sel = document.getElementById('history-playbackspeed'); - if (sel) sel.removeEventListener('change', this.playbackChangeListener); - this.playbackChangeListener = null; - } - if (this.followChangeListener) { - const cb = document.getElementById('history-options-followContents'); - if (cb) cb.removeEventListener('change', this.followChangeListener); - this.followChangeListener = null; - } - // Inner BroadcastSlider has no removeCallback API, but the whole iframe - // is destroyed on exit so any callbacks die with it. + // Every outer Settings/history control we bound is tracked in one list, + // so a single pass tears them all down. (The inner BroadcastSlider has no + // removeCallback API, but the whole iframe 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 = []; + } + + // Attach a listener to an outer control and register it for teardown on + // exit. No-ops if the element is missing so callers can stay terse. + private bindOuter(el: HTMLElement | null, type: string, fn: EventListener): void { + if (!el) return; + el.addEventListener(type, fn); + this.outerControlListeners.push({el, type, fn}); } private mountIframe(rev: number | null): void { @@ -344,27 +334,21 @@ class PadModeController { const rightStep = document.getElementById('history-rightstep') as HTMLButtonElement | null; const timer = document.getElementById('history-timer') as HTMLElement | null; - const bind = (el: HTMLElement | null, type: string, fn: EventListener) => { - if (!el) return; - el.addEventListener(type, fn); - this.outerControlListeners.push({el, type, fn}); - }; - - bind(sliderInput, 'input', () => { + this.bindOuter(sliderInput, 'input', () => { if (!sliderInput) return; const target = Math.max(0, Math.floor(Number(sliderInput.value) || 0)); try { inner.BroadcastSlider?.setSliderPosition?.(target); } catch (_e) {} }); - bind(playBtn, 'click', () => { + this.bindOuter(playBtn, 'click', () => { try { inner.BroadcastSlider?.playpause?.(); } catch (_e) {} }); // Inner #leftstep / #rightstep already wire all the step logic; just // forward the click so we share the same code path. - bind(leftStep, 'click', () => { + this.bindOuter(leftStep, 'click', () => { try { (innerWin.document.getElementById('leftstep') as HTMLElement | null)?.click(); } catch (_e) {} }); - bind(rightStep, 'click', () => { + this.bindOuter(rightStep, 'click', () => { try { (innerWin.document.getElementById('rightstep') as HTMLElement | null)?.click(); } catch (_e) {} }); @@ -510,15 +494,15 @@ class PadModeController { // BroadcastSlider state from those controls so the user sees one set of // controls regardless of mode. private wireSettingsBridges(innerWin: Window): void { + const inner: any = innerWin as any; const speedSel = document.getElementById('history-playbackspeed') as HTMLSelectElement | null; const followCb = document.getElementById('history-options-followContents') as HTMLInputElement | null; - const inner: any = innerWin as any; if (speedSel) { // Initial sync: read existing inner cookie/setting if available. const innerSpeed = inner.document.getElementById('playbackspeed') as HTMLSelectElement | null; if (innerSpeed && innerSpeed.value) speedSel.value = innerSpeed.value; - this.playbackChangeListener = () => { + this.bindOuter(speedSel, 'change', () => { const v = speedSel.value || '100'; try { inner.BroadcastSlider?.setPlaybackSpeed?.(v); @@ -527,53 +511,34 @@ class PadModeController { innerSpeed.dispatchEvent(new Event('change')); } } catch (_e) {} - }; - speedSel.addEventListener('change', this.playbackChangeListener); + }); } if (followCb) { const innerFollow = inner.document.getElementById('options-followContents') as HTMLInputElement | null; if (innerFollow) followCb.checked = !!innerFollow.checked; - this.followChangeListener = () => { + this.bindOuter(followCb, 'change', () => { if (!innerFollow) return; innerFollow.checked = followCb.checked; innerFollow.dispatchEvent(new Event('change')); - }; - 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); + // Authorship colours, font family and line numbers each appear in two + // places in the outer Settings UI (the legacy popup ids and the + // `#padsettings-…` pane), so bridge every id to the embedded slider's + // matching view-setting method. + const bridgeView = (ids: string[], apply: (el: T) => void) => + ids.forEach((id) => { + const el = document.getElementById(id) as T | null; + this.bindOuter(el, 'change', () => { try { apply(el!); } catch (_e) {} }); + }); + bridgeView(['options-colorscheck', 'padsettings-options-colorscheck'], + (cb) => inner.BroadcastSlider?.setShowAuthorColors?.(cb.checked)); + bridgeView(['viewfontmenu', 'padsettings-viewfontmenu'], + (sel) => inner.BroadcastSlider?.setPadFontFamily?.(sel.value)); + bridgeView(['options-linenoscheck', 'padsettings-options-linenoscheck'], + (cb) => inner.BroadcastSlider?.setShowLineNumbers?.(cb.checked)); } private setInnerRevision(rev: number): void { diff --git a/src/static/js/timeslider.ts b/src/static/js/timeslider.ts index 35f0b5cfd43..155786e4e17 100644 --- a/src/static/js/timeslider.ts +++ b/src/static/js/timeslider.ts @@ -72,6 +72,12 @@ const applyShowAuthorColors = (showAuthorColors) => { $('#sidedivinner').toggleClass('authorColors', showAuthorColors); }; +// Pass '' (not null) to clear the rule — jQuery 3 ignores a null css value, +// so the inline font-family would otherwise stick on reset. +const applyPadFontFamily = (fontFamily) => { + $('#innerdocbody').css('font-family', fontFamily || ''); +}; + const init = () => { padutils.setupGlobalExceptionHandler(); $(document).ready(() => { @@ -198,14 +204,6 @@ 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; @@ -253,17 +251,9 @@ const handleClientVars = (message) => { }); applyShowLineNumbers(readPadPrefs().showLineNumbers !== false); - // Read authorship colors preference from cookie (set by pad editor) + // Honour the view preferences the pad editor saved to the cookie so the + // first paint matches the user's pad settings. 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); @@ -273,6 +263,16 @@ const handleClientVars = (message) => { applyPadFontFamily(fontFamily); }); + // Entry points for the outer pad shell (#7659 in-place history mode) to push + // view settings into this iframe live when the user changes them on the pad. + BroadcastSlider.setShowAuthorColors = (showAuthorColors) => { + applyShowAuthorColors(showAuthorColors); + setPadPref('showAuthorshipColors', showAuthorColors); + }; + BroadcastSlider.setShowLineNumbers = (showLineNumbers) => { + applyShowLineNumbers(showLineNumbers); + setPadPref('showLineNumbers', showLineNumbers); + }; BroadcastSlider.setPadFontFamily = (fontFamily) => { applyPadFontFamily(fontFamily); setPadPref('padFontFamily', fontFamily);