From 384751359499debd18789734930cb26dd345a3ba Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 25 Mar 2026 23:19:49 -0400 Subject: [PATCH] fix: resolve DOMContentLoaded crash + wire kanban/pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix lt.clipboard.init() → initCopyButtons() (crashed init handler before context menu, split pane, lightbox, theme btn could register) - Add lt.pagination module (Module 55) with ellipsis rendering, prev/next, onChange callback; wire demo-pagination nav - Upgrade lt.sortable for cross-list group dragging (shared module-level drag state enables kanban card movement between columns) - Wire kanban columns (open/pending/inprogress/closed) to lt.sortable with group:'kanban' for drag-and-drop between columns Co-Authored-By: Claude Sonnet 4.6 --- base.html | 32 ++++++----- base.js | 160 ++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 126 insertions(+), 66 deletions(-) diff --git a/base.html b/base.html index 6eccc38..be00f61 100644 --- a/base.html +++ b/base.html @@ -415,7 +415,7 @@
Open
-
+
@@ -443,7 +443,7 @@
Pending
-
+
P2 @@ -460,7 +460,7 @@
In Progress
-
+
P2 @@ -477,7 +477,7 @@
Closed
-
+
P4 @@ -724,15 +724,7 @@
NODE-07
- +
@@ -1573,7 +1565,7 @@ Storage array link-down on `compute-storage-01`. lt.tooltip.init(); // Clipboard copy buttons ([data-copy]) - lt.clipboard.init(); + lt.clipboard.initCopyButtons(); // Alert dismiss buttons lt.alerts.init(); @@ -1691,6 +1683,18 @@ Storage array link-down on `compute-storage-01`. }, }); + // Kanban drag-and-drop — all four columns share group "kanban" + ['kanban-col-open','kanban-col-pending','kanban-col-inprogress','kanban-col-closed'].forEach(id => { + const col = document.getElementById(id); + if (col) lt.sortable.init(col, { group: 'kanban', onSort: () => {} }); + }); + + // Pagination demo (50 items, 10 per page) + lt.pagination.init('#demo-pagination', { + total: 50, perPage: 10, page: 2, + onChange: p => lt.toast.info('Page ' + p), + }); + // Countdown demo — SLA expires 2 hours from now const slaTarget = new Date(Date.now() + 2 * 60 * 60 * 1000); lt.timer.countdown(document.getElementById('demo-countdown'), slaTarget, { diff --git a/base.js b/base.js index 18c976a..77006fd 100644 --- a/base.js +++ b/base.js @@ -2228,90 +2228,86 @@ MODULE 50 — SORTABLE (drag-to-reorder lists/kanban) lt.sortable.init(listEl, opts) opts: { handle (selector), onSort(newOrder, movedEl), group } - Returns draggable list; emits 'sortable:change' on bus + Supports cross-list dragging when group matches. + Returns { refresh(), getOrder() }; emits 'sortable:change' on bus ================================================================ */ + // Shared cross-instance drag state (enables group/cross-column dragging) + let _srtDragging = null, _srtPlaceholder = null, _srtSrcList = null; + const sortable = { init(list, opts = {}) { - const { handle = null, onSort = null, group = null, animation = 200 } = opts; + const { handle = null, onSort = null, group = null } = opts; list.setAttribute('data-sortable-group', group || ''); - let _dragging = null, _placeholder = null, _startIdx = -1; - function _idx(el) { return Array.from(list.children).indexOf(el); } - - function _makePlaceholder(el) { - const ph = document.createElement(el.tagName); - ph.className = 'lt-sortable-placeholder'; - ph.style.height = el.offsetHeight + 'px'; - ph.style.width = el.offsetWidth + 'px'; - return ph; - } - - function _getItems() { return Array.from(list.querySelectorAll(':scope > [data-sortable-item]')); } - - // Mark all children as sortable items - Array.from(list.children).forEach(child => { + function _mark(child) { child.setAttribute('data-sortable-item', ''); child.setAttribute('draggable', handle ? 'false' : 'true'); if (handle) { const h = child.querySelector(handle); if (h) { h.setAttribute('draggable', 'true'); h.style.cursor = 'grab'; } } else { child.style.cursor = 'grab'; } - }); + } + + function _getItems() { return Array.from(list.querySelectorAll(':scope > [data-sortable-item]')); } + + function _makePlaceholder(el) { + const ph = document.createElement(el.tagName); + ph.className = 'lt-sortable-placeholder'; + ph.style.height = el.offsetHeight + 'px'; + ph.style.width = '100%'; + return ph; + } + + function _sameGroup(otherList) { + if (!group) return false; + return otherList.getAttribute('data-sortable-group') === group; + } + + // Mark all current children + Array.from(list.children).forEach(_mark); list.addEventListener('dragstart', e => { const item = e.target.closest('[data-sortable-item]'); if (!item || !list.contains(item)) return; - _dragging = item; - _startIdx = _idx(item); - _placeholder = _makePlaceholder(item); + _srtDragging = item; + _srtSrcList = list; + _srtPlaceholder = _makePlaceholder(item); requestAnimationFrame(() => { item.classList.add('is-dragging'); }); e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', ''); // Firefox compat + e.dataTransfer.setData('text/plain', ''); }); list.addEventListener('dragend', () => { - if (!_dragging) return; - _dragging.classList.remove('is-dragging'); - _dragging.style.opacity = ''; - if (_placeholder && _placeholder.parentNode) { - _placeholder.parentNode.insertBefore(_dragging, _placeholder); - _placeholder.remove(); + if (!_srtDragging) return; + _srtDragging.classList.remove('is-dragging'); + if (_srtPlaceholder && _srtPlaceholder.parentNode) { + _srtPlaceholder.parentNode.insertBefore(_srtDragging, _srtPlaceholder); + _srtPlaceholder.remove(); } - const endIdx = _idx(_dragging); - if (endIdx !== _startIdx && onSort) onSort(_getItems(), _dragging); - bus.emit('sortable:change', { list, items: _getItems(), moved: _dragging }); - _dragging = null; _placeholder = null; + if (onSort) onSort(_getItems(), _srtDragging); + bus.emit('sortable:change', { list, items: _getItems(), moved: _srtDragging }); + _srtDragging = null; _srtPlaceholder = null; _srtSrcList = null; }); list.addEventListener('dragover', e => { + if (!_srtDragging) return; + // Allow drop only within same list or same group + if (list !== _srtSrcList && !_sameGroup(_srtSrcList)) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; + if (!_srtPlaceholder) _srtPlaceholder = _makePlaceholder(_srtDragging); const over = e.target.closest('[data-sortable-item]'); - if (!over || over === _dragging || !list.contains(over)) return; - if (!_placeholder) return; - const rect = over.getBoundingClientRect(); - const mid = rect.top + rect.height / 2; - over.parentNode.insertBefore(_placeholder, e.clientY < mid ? over : over.nextSibling); - }); - - list.addEventListener('dragenter', e => { - const over = e.target.closest('[data-sortable-item]'); - if (over && over !== _dragging && list.contains(over) && !_placeholder) { - _placeholder = _makePlaceholder(_dragging); + if (over && over !== _srtDragging && list.contains(over)) { + const rect = over.getBoundingClientRect(); + list.insertBefore(_srtPlaceholder, e.clientY < rect.top + rect.height / 2 ? over : over.nextSibling); + } else if (!list.contains(_srtPlaceholder)) { + list.appendChild(_srtPlaceholder); } }); list.addEventListener('drop', e => { e.preventDefault(); }); return { - refresh() { - Array.from(list.children).forEach(child => { - if (!child.hasAttribute('data-sortable-item')) { - child.setAttribute('data-sortable-item', ''); - child.setAttribute('draggable', handle ? 'false' : 'true'); - if (!handle) child.style.cursor = 'grab'; - } - }); - }, + refresh() { Array.from(list.children).forEach(child => { if (!child.hasAttribute('data-sortable-item')) _mark(child); }); }, getOrder: () => _getItems().map(el => el.dataset.id || el.textContent.trim()), }; }, @@ -2621,6 +2617,65 @@ }, }; + /* ================================================================ + MODULE 55 — PAGINATION + lt.pagination.init(navEl, opts) + opts: { total, perPage, page, onChange(page) } + Renders page buttons inside navEl; re-renders on page change. + ================================================================ */ + const pagination = { + init(nav, opts = {}) { + if (typeof nav === 'string') nav = document.querySelector(nav); + if (!nav) return null; + let { total = 0, perPage = 10, page = 1, onChange = null, maxBtns = 7 } = opts; + + function _pages() { return Math.max(1, Math.ceil(total / perPage)); } + + function render() { + const pages = _pages(); + let html = ''; + // Prev + html += ``; + // Page buttons with ellipsis + const half = Math.floor((maxBtns - 2) / 2); + let start = Math.max(2, page - half); + let end = Math.min(pages - 1, page + half); + if (end - start < maxBtns - 3) { + if (start === 2) end = Math.min(pages - 1, start + maxBtns - 3); + else start = Math.max(2, end - maxBtns + 3); + } + html += ``; + if (start > 2) html += ``; + for (let i = start; i <= end; i++) { + html += ``; + } + if (end < pages - 1) html += ``; + if (pages > 1) html += ``; + // Next + html += ``; + nav.innerHTML = html; + } + + nav.addEventListener('click', e => { + const btn = e.target.closest('.lt-page-btn'); + if (!btn || btn.disabled || btn.classList.contains('active')) return; + const p = parseInt(btn.dataset.page, 10); + if (!p || p < 1 || p > _pages()) return; + page = p; + render(); + if (onChange) onChange(page); + }); + + render(); + return { + setTotal(n) { total = n; page = 1; render(); }, + setPage(p) { page = Math.max(1, Math.min(_pages(), p)); render(); }, + getPage() { return page; }, + getPages() { return _pages(); }, + }; + }, + }; + /* ================================================================ PUBLIC API ---------------------------------------------------------------- */ @@ -2683,6 +2738,7 @@ lightbox, auth, markdown, + pagination, sidebarSubmenus: { init: initSidebarSubmenus }, };