fix: resolve DOMContentLoaded crash + wire kanban/pagination
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -415,7 +415,7 @@
|
|||||||
<span class="lt-frame-bl">╚</span>
|
<span class="lt-frame-bl">╚</span>
|
||||||
<span class="lt-frame-br">╝</span>
|
<span class="lt-frame-br">╝</span>
|
||||||
<div class="lt-section-header">Open</div>
|
<div class="lt-section-header">Open</div>
|
||||||
<div class="lt-section-body">
|
<div class="lt-section-body" id="kanban-col-open" style="min-height:60px">
|
||||||
|
|
||||||
<div class="lt-card lt-mb-md lt-row-p1">
|
<div class="lt-card lt-mb-md lt-row-p1">
|
||||||
<div class="lt-flex lt-flex-between lt-mb-md">
|
<div class="lt-flex lt-flex-between lt-mb-md">
|
||||||
@@ -443,7 +443,7 @@
|
|||||||
<span class="lt-frame-bl">╚</span>
|
<span class="lt-frame-bl">╚</span>
|
||||||
<span class="lt-frame-br">╝</span>
|
<span class="lt-frame-br">╝</span>
|
||||||
<div class="lt-section-header">Pending</div>
|
<div class="lt-section-header">Pending</div>
|
||||||
<div class="lt-section-body">
|
<div class="lt-section-body" id="kanban-col-pending" style="min-height:60px">
|
||||||
<div class="lt-card lt-row-p2">
|
<div class="lt-card lt-row-p2">
|
||||||
<div class="lt-flex lt-flex-between lt-mb-md">
|
<div class="lt-flex lt-flex-between lt-mb-md">
|
||||||
<span class="lt-p2">P2</span>
|
<span class="lt-p2">P2</span>
|
||||||
@@ -460,7 +460,7 @@
|
|||||||
<span class="lt-frame-bl">╚</span>
|
<span class="lt-frame-bl">╚</span>
|
||||||
<span class="lt-frame-br">╝</span>
|
<span class="lt-frame-br">╝</span>
|
||||||
<div class="lt-section-header">In Progress</div>
|
<div class="lt-section-header">In Progress</div>
|
||||||
<div class="lt-section-body">
|
<div class="lt-section-body" id="kanban-col-inprogress" style="min-height:60px">
|
||||||
<div class="lt-card lt-row-p2 lt-item-running">
|
<div class="lt-card lt-row-p2 lt-item-running">
|
||||||
<div class="lt-flex lt-flex-between lt-mb-md">
|
<div class="lt-flex lt-flex-between lt-mb-md">
|
||||||
<span class="lt-p2">P2</span>
|
<span class="lt-p2">P2</span>
|
||||||
@@ -477,7 +477,7 @@
|
|||||||
<span class="lt-frame-bl">╚</span>
|
<span class="lt-frame-bl">╚</span>
|
||||||
<span class="lt-frame-br">╝</span>
|
<span class="lt-frame-br">╝</span>
|
||||||
<div class="lt-section-header">Closed</div>
|
<div class="lt-section-header">Closed</div>
|
||||||
<div class="lt-section-body">
|
<div class="lt-section-body" id="kanban-col-closed" style="min-height:60px">
|
||||||
<div class="lt-card lt-row-p4" style="opacity:0.6">
|
<div class="lt-card lt-row-p4" style="opacity:0.6">
|
||||||
<div class="lt-flex lt-flex-between lt-mb-md">
|
<div class="lt-flex lt-flex-between lt-mb-md">
|
||||||
<span class="lt-p4">P4</span>
|
<span class="lt-p4">P4</span>
|
||||||
@@ -724,15 +724,7 @@
|
|||||||
<div class="lt-breadcrumb-sep"></div>
|
<div class="lt-breadcrumb-sep"></div>
|
||||||
<div class="lt-breadcrumb-item active">NODE-07</div>
|
<div class="lt-breadcrumb-item active">NODE-07</div>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="lt-pagination" aria-label="pagination">
|
<nav class="lt-pagination" id="demo-pagination" aria-label="pagination"></nav>
|
||||||
<button class="lt-page-btn" disabled>«</button>
|
|
||||||
<button class="lt-page-btn" disabled>1</button>
|
|
||||||
<button class="lt-page-btn active">2</button>
|
|
||||||
<button class="lt-page-btn">3</button>
|
|
||||||
<button class="lt-page-btn">4</button>
|
|
||||||
<button class="lt-page-btn">5</button>
|
|
||||||
<button class="lt-page-btn">»</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ACCORDION -->
|
<!-- ACCORDION -->
|
||||||
@@ -1573,7 +1565,7 @@ Storage array link-down on `compute-storage-01`.
|
|||||||
lt.tooltip.init();
|
lt.tooltip.init();
|
||||||
|
|
||||||
// Clipboard copy buttons ([data-copy])
|
// Clipboard copy buttons ([data-copy])
|
||||||
lt.clipboard.init();
|
lt.clipboard.initCopyButtons();
|
||||||
|
|
||||||
// Alert dismiss buttons
|
// Alert dismiss buttons
|
||||||
lt.alerts.init();
|
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
|
// Countdown demo — SLA expires 2 hours from now
|
||||||
const slaTarget = new Date(Date.now() + 2 * 60 * 60 * 1000);
|
const slaTarget = new Date(Date.now() + 2 * 60 * 60 * 1000);
|
||||||
lt.timer.countdown(document.getElementById('demo-countdown'), slaTarget, {
|
lt.timer.countdown(document.getElementById('demo-countdown'), slaTarget, {
|
||||||
|
|||||||
@@ -2228,90 +2228,86 @@
|
|||||||
MODULE 50 — SORTABLE (drag-to-reorder lists/kanban)
|
MODULE 50 — SORTABLE (drag-to-reorder lists/kanban)
|
||||||
lt.sortable.init(listEl, opts)
|
lt.sortable.init(listEl, opts)
|
||||||
opts: { handle (selector), onSort(newOrder, movedEl), group }
|
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 = {
|
const sortable = {
|
||||||
init(list, opts = {}) {
|
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 || '');
|
list.setAttribute('data-sortable-group', group || '');
|
||||||
let _dragging = null, _placeholder = null, _startIdx = -1;
|
|
||||||
|
|
||||||
function _idx(el) { return Array.from(list.children).indexOf(el); }
|
function _mark(child) {
|
||||||
|
|
||||||
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 => {
|
|
||||||
child.setAttribute('data-sortable-item', '');
|
child.setAttribute('data-sortable-item', '');
|
||||||
child.setAttribute('draggable', handle ? 'false' : 'true');
|
child.setAttribute('draggable', handle ? 'false' : 'true');
|
||||||
if (handle) {
|
if (handle) {
|
||||||
const h = child.querySelector(handle);
|
const h = child.querySelector(handle);
|
||||||
if (h) { h.setAttribute('draggable', 'true'); h.style.cursor = 'grab'; }
|
if (h) { h.setAttribute('draggable', 'true'); h.style.cursor = 'grab'; }
|
||||||
} else { child.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 => {
|
list.addEventListener('dragstart', e => {
|
||||||
const item = e.target.closest('[data-sortable-item]');
|
const item = e.target.closest('[data-sortable-item]');
|
||||||
if (!item || !list.contains(item)) return;
|
if (!item || !list.contains(item)) return;
|
||||||
_dragging = item;
|
_srtDragging = item;
|
||||||
_startIdx = _idx(item);
|
_srtSrcList = list;
|
||||||
_placeholder = _makePlaceholder(item);
|
_srtPlaceholder = _makePlaceholder(item);
|
||||||
requestAnimationFrame(() => { item.classList.add('is-dragging'); });
|
requestAnimationFrame(() => { item.classList.add('is-dragging'); });
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
e.dataTransfer.setData('text/plain', ''); // Firefox compat
|
e.dataTransfer.setData('text/plain', '');
|
||||||
});
|
});
|
||||||
|
|
||||||
list.addEventListener('dragend', () => {
|
list.addEventListener('dragend', () => {
|
||||||
if (!_dragging) return;
|
if (!_srtDragging) return;
|
||||||
_dragging.classList.remove('is-dragging');
|
_srtDragging.classList.remove('is-dragging');
|
||||||
_dragging.style.opacity = '';
|
if (_srtPlaceholder && _srtPlaceholder.parentNode) {
|
||||||
if (_placeholder && _placeholder.parentNode) {
|
_srtPlaceholder.parentNode.insertBefore(_srtDragging, _srtPlaceholder);
|
||||||
_placeholder.parentNode.insertBefore(_dragging, _placeholder);
|
_srtPlaceholder.remove();
|
||||||
_placeholder.remove();
|
|
||||||
}
|
}
|
||||||
const endIdx = _idx(_dragging);
|
if (onSort) onSort(_getItems(), _srtDragging);
|
||||||
if (endIdx !== _startIdx && onSort) onSort(_getItems(), _dragging);
|
bus.emit('sortable:change', { list, items: _getItems(), moved: _srtDragging });
|
||||||
bus.emit('sortable:change', { list, items: _getItems(), moved: _dragging });
|
_srtDragging = null; _srtPlaceholder = null; _srtSrcList = null;
|
||||||
_dragging = null; _placeholder = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
list.addEventListener('dragover', e => {
|
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';
|
e.preventDefault(); e.dataTransfer.dropEffect = 'move';
|
||||||
|
if (!_srtPlaceholder) _srtPlaceholder = _makePlaceholder(_srtDragging);
|
||||||
const over = e.target.closest('[data-sortable-item]');
|
const over = e.target.closest('[data-sortable-item]');
|
||||||
if (!over || over === _dragging || !list.contains(over)) return;
|
if (over && over !== _srtDragging && list.contains(over)) {
|
||||||
if (!_placeholder) return;
|
const rect = over.getBoundingClientRect();
|
||||||
const rect = over.getBoundingClientRect();
|
list.insertBefore(_srtPlaceholder, e.clientY < rect.top + rect.height / 2 ? over : over.nextSibling);
|
||||||
const mid = rect.top + rect.height / 2;
|
} else if (!list.contains(_srtPlaceholder)) {
|
||||||
over.parentNode.insertBefore(_placeholder, e.clientY < mid ? over : over.nextSibling);
|
list.appendChild(_srtPlaceholder);
|
||||||
});
|
|
||||||
|
|
||||||
list.addEventListener('dragenter', e => {
|
|
||||||
const over = e.target.closest('[data-sortable-item]');
|
|
||||||
if (over && over !== _dragging && list.contains(over) && !_placeholder) {
|
|
||||||
_placeholder = _makePlaceholder(_dragging);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
list.addEventListener('drop', e => { e.preventDefault(); });
|
list.addEventListener('drop', e => { e.preventDefault(); });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
refresh() {
|
refresh() { Array.from(list.children).forEach(child => { if (!child.hasAttribute('data-sortable-item')) _mark(child); }); },
|
||||||
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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getOrder: () => _getItems().map(el => el.dataset.id || el.textContent.trim()),
|
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 += `<button class="lt-page-btn" ${page <= 1 ? 'disabled' : ''} data-page="${page - 1}">«</button>`;
|
||||||
|
// 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 += `<button class="lt-page-btn${page === 1 ? ' active' : ''}" data-page="1">1</button>`;
|
||||||
|
if (start > 2) html += `<button class="lt-page-btn" disabled>…</button>`;
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
html += `<button class="lt-page-btn${page === i ? ' active' : ''}" data-page="${i}">${i}</button>`;
|
||||||
|
}
|
||||||
|
if (end < pages - 1) html += `<button class="lt-page-btn" disabled>…</button>`;
|
||||||
|
if (pages > 1) html += `<button class="lt-page-btn${page === pages ? ' active' : ''}" data-page="${pages}">${pages}</button>`;
|
||||||
|
// Next
|
||||||
|
html += `<button class="lt-page-btn" ${page >= pages ? 'disabled' : ''} data-page="${page + 1}">»</button>`;
|
||||||
|
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
|
PUBLIC API
|
||||||
---------------------------------------------------------------- */
|
---------------------------------------------------------------- */
|
||||||
@@ -2683,6 +2738,7 @@
|
|||||||
lightbox,
|
lightbox,
|
||||||
auth,
|
auth,
|
||||||
markdown,
|
markdown,
|
||||||
|
pagination,
|
||||||
sidebarSubmenus: { init: initSidebarSubmenus },
|
sidebarSubmenus: { init: initSidebarSubmenus },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user