mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
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
This commit is contained in:
parent
2209e37857
commit
226487b009
5 changed files with 167 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
6
src/static/js/vendors/nice-select.ts
vendored
6
src/static/js/vendors/nice-select.ts
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue