From aeed519df03ad752048ea32fea3e722eef16e472 Mon Sep 17 00:00:00 2001 From: Laura Date: Thu, 4 Sep 2025 17:15:09 +0200 Subject: [PATCH] draggable tables --- static/css/markdown.css | 18 +++++++- static/js/markdown.js | 97 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 106 insertions(+), 9 deletions(-) diff --git a/static/css/markdown.css b/static/css/markdown.css index ee9a8fd..151c6f7 100644 --- a/static/css/markdown.css +++ b/static/css/markdown.css @@ -91,10 +91,26 @@ border-radius: 4px; } +.markdown .table-wrapper { + width: 100%; + overflow-x: auto; + margin: 16px 0; + cursor: grab; + touch-action: pan-y; +} + +.markdown .table-wrapper:not(.overflowing) { + cursor: default; + overflow-x: hidden; +} + +.markdown .table-wrapper.dragging { + cursor: grabbing; +} + .markdown table { width: 100%; border-collapse: collapse; - margin: 16px 0; font-size: 14px; } diff --git a/static/js/markdown.js b/static/js/markdown.js index 9f41cfa..5760bbf 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -1,5 +1,12 @@ (() => { - const timeouts = new WeakMap(); + const timeouts = new WeakMap(), + scrollState = { + el: null, + startX: 0, + scrollLeft: 0, + pointerId: null, + moved: false, + }; marked.use({ async: false, @@ -7,13 +14,13 @@ gfm: true, pedantic: false, - walkTokens: (token) => { + walkTokens: token => { const { type, text } = token; if (type === "html") { - token.text = token.text.replace(/&/g, "&") - token.text = token.text.replace(//g, ">") + token.text = token.text.replace(/&/g, "&"); + token.text = token.text.replace(//g, ">"); return; } else if (type !== "code") { @@ -48,9 +55,18 @@ return `${escapeHtml(link.text || link.href)}`; }, }, + + hooks: { + postprocess: html => { + html = html.replace(//g, `
`); + html = html.replace(/<\/ ?table>/g, `
`); + + return html; + }, + }, }); - document.body.addEventListener("click", (event) => { + addEventListener("click", event => { const button = event.target, header = button.closest(".pre-header"), pre = header?.closest("pre"), @@ -70,11 +86,76 @@ pre, setTimeout(() => { button.classList.remove("copied"); - }, 1000), + }, 1000) ); }); - window.render = (markdown) => { + addEventListener("pointerover", event => { + if (event.pointerType !== "mouse") { + return; + } + + const el = event.target.closest(".table-wrapper"); + + if (!el) { + return; + } + + el.classList.toggle("overflowing", el.scrollWidth - el.clientWidth > 1); + }); + + addEventListener("pointerdown", event => { + if (event.button !== 0) { + return; + } + + const el = event.target.closest(".table-wrapper"); + + if (!el) { + return; + } + + scrollState.el = el; + scrollState.pointerId = event.pointerId; + scrollState.startX = event.clientX; + scrollState.scrollLeft = el.scrollLeft; + scrollState.moved = false; + + el.classList.add("dragging"); + el.setPointerCapture?.(event.pointerId); + + event.preventDefault(); + }); + + addEventListener("pointermove", event => { + if (!scrollState.el || event.pointerId !== scrollState.pointerId) { + return; + } + + const dx = event.clientX - scrollState.startX; + + if (Math.abs(dx) > 3) { + scrollState.moved = true; + } + + scrollState.el.scrollLeft = scrollState.scrollLeft - dx; + }); + + function endScroll(event) { + if (!scrollState.el || (event && event.pointerId !== scrollState.pointerId)) { + return; + } + + scrollState.el.classList.remove("dragging"); + scrollState.el.releasePointerCapture?.(scrollState.pointerId); + scrollState.el = null; + scrollState.pointerId = null; + } + + addEventListener("pointerup", endScroll); + addEventListener("pointercancel", endScroll); + + window.render = markdown => { return marked.parse(markdown); }; })();