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 },
};