From 79b9d86dbc007e0e023d69859db76689afe666a9 Mon Sep 17 00:00:00 2001
From: John McLear
Date: Sat, 18 Apr 2026 11:57:06 +0100
Subject: [PATCH 1/2] feat: add timeslider line numbers
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/static/css/timeslider.css | 24 ++++++-
src/static/js/broadcast.ts | 67 +++++++++++++++++
src/static/js/timeslider.ts | 39 +++++++++-
src/static/skins/colibris/timeslider.css | 12 +++-
src/templates/timeslider.html | 8 +++
.../specs/timeslider_line_numbers.spec.ts | 72 +++++++++++++++++++
6 files changed, 217 insertions(+), 5 deletions(-)
create mode 100644 src/tests/frontend-new/specs/timeslider_line_numbers.spec.ts
diff --git a/src/static/css/timeslider.css b/src/static/css/timeslider.css
index 252248b339a..65506021ec0 100644
--- a/src/static/css/timeslider.css
+++ b/src/static/css/timeslider.css
@@ -120,15 +120,33 @@
overflow-y: auto;
}
#outerdocbody {
- display: block;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-start;
width: 100%;
}
+#outerdocbody > #sidediv {
+ flex: 0 0 auto;
+ padding-top: 30px;
+ padding-bottom: 30px;
+}
+
+#outerdocbody > #innerdocbody {
+ flex: 1 1 auto;
+ min-width: 0;
+ padding-right: calc(var(--editor-horizontal-padding, 0px) + 15px);
+ padding-top: 30px;
+ padding-left: calc(var(--editor-horizontal-padding, 0px) + 15px);
+ padding-bottom: 30px;
+}
+
#innerdocbody {
white-space: normal;
word-break: break-word;
width: 100%;
- margin: 0 auto;
+ margin: 0;
height: auto;
}
@@ -151,4 +169,4 @@
display: flex;
flex-wrap: wrap;
}
-}
\ No newline at end of file
+}
diff --git a/src/static/js/broadcast.ts b/src/static/js/broadcast.ts
index 01e4a2edf01..9d236f70f78 100644
--- a/src/static/js/broadcast.ts
+++ b/src/static/js/broadcast.ts
@@ -132,6 +132,66 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
},
};
+ const targetBody = document.getElementById('innerdocbody');
+ const sideDiv = document.getElementById('sidediv');
+ const sideDivInner = document.getElementById('sidedivinner');
+ const appendNewSideDivLine = () => {
+ const lineDiv = document.createElement('div');
+ sideDivInner.appendChild(lineDiv);
+ const lineSpan = document.createElement('span');
+ lineSpan.classList.add('line-number');
+ lineSpan.appendChild(document.createTextNode(sideDivInner.children.length));
+ lineDiv.appendChild(lineSpan);
+ };
+
+ const updateLineNumbers = () => {
+ if (!targetBody || !sideDiv || !sideDivInner) return;
+ const lineOffsets = [];
+ const lineHeights = [];
+ const innerdocbodyStyles = getComputedStyle(targetBody);
+ const defaultLineHeight = parseInt(innerdocbodyStyles.lineHeight);
+
+ for (const docLine of targetBody.children) {
+ let height;
+ const nextDocLine = docLine.nextElementSibling;
+ if (nextDocLine) {
+ if (lineOffsets.length === 0) {
+ height = nextDocLine.offsetTop - parseInt(
+ innerdocbodyStyles.getPropertyValue('padding-top'));
+ } else {
+ height = nextDocLine.offsetTop - docLine.offsetTop;
+ }
+ } else {
+ height = docLine.clientHeight || docLine.offsetHeight;
+ }
+ lineOffsets.push(height);
+
+ if (docLine.clientHeight !== defaultLineHeight && docLine.firstElementChild != null) {
+ const elementStyle = window.getComputedStyle(docLine.firstElementChild);
+ const lineHeight = parseInt(elementStyle.getPropertyValue('line-height'));
+ const marginBottom = parseInt(elementStyle.getPropertyValue('margin-bottom'));
+ lineHeights.push(lineHeight + marginBottom);
+ } else {
+ lineHeights.push(defaultLineHeight);
+ }
+ }
+
+ const newNumLines = Math.max(targetBody.children.length, 1);
+ while (sideDivInner.children.length < newNumLines) appendNewSideDivLine();
+ while (sideDivInner.children.length > newNumLines) sideDivInner.lastElementChild.remove();
+ for (const [i, sideDivLine] of Array.prototype.entries.call(sideDivInner.children)) {
+ sideDivLine.style.height = `${lineOffsets[i]}px`;
+ sideDivLine.style.lineHeight = `${lineHeights[i]}px`;
+ }
+ $(sideDiv).addClass('sidedivdelayed');
+ };
+
+ const scheduleLineNumberUpdate = () => {
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(updateLineNumbers);
+ });
+ };
+
const applyChangeset = (changeset, revision, preventSliderMovement, timeDelta) => {
// disable the next 'gotorevision' call handled by a timeslider update
if (!preventSliderMovement) {
@@ -194,6 +254,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
padContents.currentRevision = revision;
padContents.currentTime += timeDelta;
+ updateLineNumbers();
updateTimer();
@@ -465,6 +526,12 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
padContents.currentDivs.push(div);
$('#innerdocbody').append(div);
}
+ updateLineNumbers();
+ scheduleLineNumberUpdate();
+ $(window).on('resize', scheduleLineNumberUpdate);
+ window.addEventListener('load', scheduleLineNumberUpdate, {once: true});
+ document.fonts?.ready?.then(scheduleLineNumberUpdate);
+ $('#viewfontmenu').on('change', () => window.setTimeout(scheduleLineNumberUpdate, 0));
});
// this is necessary to keep infinite loops of events firing,
diff --git a/src/static/js/timeslider.ts b/src/static/js/timeslider.ts
index f065aee4112..d0e45973f96 100644
--- a/src/static/js/timeslider.ts
+++ b/src/static/js/timeslider.ts
@@ -36,6 +36,37 @@ let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
let cp = '';
const playbackSpeedCookie = 'timesliderPlaybackSpeed';
+const getPrefsCookieName = () => `${cp}${window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp'}`;
+
+const readPadPrefs = () => {
+ try {
+ let json = Cookies.get(getPrefsCookieName());
+ if (json == null) {
+ const unprefixed = window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp';
+ if (unprefixed !== getPrefsCookieName()) json = Cookies.get(unprefixed);
+ }
+ return json == null ? {} : JSON.parse(json);
+ } catch (err) {
+ return {};
+ }
+};
+
+const writePadPrefs = (prefs) => {
+ Cookies.set(getPrefsCookieName(), JSON.stringify(prefs), {expires: 365 * 100});
+};
+
+const setPadPref = (prefName, value) => {
+ const prefs = readPadPrefs();
+ prefs[prefName] = value;
+ writePadPrefs(prefs);
+};
+
+const applyShowLineNumbers = (showLineNumbers) => {
+ padutils.setCheckbox($('#options-linenoscheck'), showLineNumbers);
+ $('body').toggleClass('line-numbers-hidden', !showLineNumbers);
+ window.requestAnimationFrame(() => $(window).trigger('resize'));
+};
+
const init = () => {
padutils.setupGlobalExceptionHandler();
$(document).ready(() => {
@@ -113,7 +144,7 @@ const fireWhenAllScriptsAreLoaded = [];
const handleClientVars = (message) => {
// save the client Vars
window.clientVars = message.data;
- cp = window.clientVars.cookiePrefix || '';
+ cp = (window as any).clientVars?.cookiePrefix || '';
if (window.clientVars.sessionRefreshInterval) {
const ping =
@@ -169,6 +200,12 @@ const handleClientVars = (message) => {
$('#playpause_button_icon').attr('title', html10n.get('timeslider.playPause'));
$('#leftstep').attr('title', html10n.get('timeslider.backRevision'));
$('#rightstep').attr('title', html10n.get('timeslider.forwardRevision'));
+ padutils.bindCheckboxChange($('#options-linenoscheck'), () => {
+ const showLineNumbers = padutils.getCheckbox('#options-linenoscheck');
+ setPadPref('showLineNumbers', showLineNumbers);
+ applyShowLineNumbers(showLineNumbers);
+ });
+ applyShowLineNumbers(readPadPrefs().showLineNumbers !== false);
// font family change
$('#viewfontmenu').on('change', function () {
diff --git a/src/static/skins/colibris/timeslider.css b/src/static/skins/colibris/timeslider.css
index 263e0e592a7..dc52de73c34 100644
--- a/src/static/skins/colibris/timeslider.css
+++ b/src/static/skins/colibris/timeslider.css
@@ -82,6 +82,16 @@
font-size: .9em;
}
+.timeslider #outerdocbody > #sidediv {
+ padding-top: 30px;
+ padding-bottom: 30px;
+}
+
+.timeslider #outerdocbody > #innerdocbody {
+ padding-top: 30px;
+ padding-bottom: 30px;
+}
+
@media (max-width: 800px) {
#slider-btn-container {
@@ -95,4 +105,4 @@
#slider-btn-container #playpause_button_icon:before {
font-size: 18px;
}
-}
\ No newline at end of file
+}
diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html
index 08fa3e4b443..393c1255a87 100644
--- a/src/templates/timeslider.html
+++ b/src/templates/timeslider.html
@@ -110,8 +110,12 @@
@@ -248,6 +252,10 @@
+
+
+
+