list: drag-to-resize and drag-to-reorder for columns
Mirrors the grid module's UX in Frappe's desk list view — drag the right edge of a header to resize, drag the header itself to reorder. Widths and order persist per doctype in localStorage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe9dc89eb6
commit
4e038cf050
|
|
@ -249,6 +249,51 @@ input.list-header-checkbox:checked::after {
|
|||
background: rgba(36, 144, 239, 0.05);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
List View Column Resize + Reorder
|
||||
========================================================================== */
|
||||
|
||||
.list-row-head .list-row-col {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Column dividers — hint that headers can be resized / reordered. */
|
||||
.list-row-head .list-row-col:not(:last-child) {
|
||||
border-right: 1px solid var(--border-color, #e2e6e9);
|
||||
}
|
||||
|
||||
.list-row-head .jey-col-resize {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -2px;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 3;
|
||||
background: transparent;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.list-row-head .jey-col-resize:hover,
|
||||
.list-row-head .jey-col-resize:active {
|
||||
background: var(--primary, #2490ef);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.list-row-head .list-row-col[data-jey-draggable]:not(.list-subject) {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.list-row-head .list-row-col.jey-drop-target.jey-drop-left {
|
||||
box-shadow: inset 3px 0 0 var(--primary, #2490ef);
|
||||
background: rgba(36, 144, 239, 0.05);
|
||||
}
|
||||
|
||||
.list-row-head .list-row-col.jey-drop-target.jey-drop-right {
|
||||
box-shadow: inset -3px 0 0 var(--primary, #2490ef);
|
||||
background: rgba(36, 144, 239, 0.05);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Sidebar — Full Width button
|
||||
========================================================================== */
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
resizeStyle.id = "jey-col-resize-styles";
|
||||
var rules = [];
|
||||
var earlyGridKeys = [];
|
||||
var earlyListDoctypes = [];
|
||||
for (var i = 0; i < localStorage.length; i++) {
|
||||
var k = localStorage.key(i);
|
||||
if (k.indexOf("jey-col-widths:") === 0) {
|
||||
|
|
@ -37,13 +38,27 @@
|
|||
);
|
||||
});
|
||||
} catch (e) {}
|
||||
} else if (k.indexOf("jey-list-col-widths:") === 0) {
|
||||
var listDoctype = k.substring(20);
|
||||
earlyListDoctypes.push(listDoctype);
|
||||
try {
|
||||
var lwidths = JSON.parse(localStorage.getItem(k));
|
||||
Object.keys(lwidths).forEach(function (fn) {
|
||||
var esc = (window.CSS && CSS.escape) ? CSS.escape(fn) : fn.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
||||
rules.push(
|
||||
'.list-view[data-jey-list="' + listDoctype + '"] .list-row-col.' + esc +
|
||||
" { flex: 0 0 " + lwidths[fn] + "px !important; width: " + lwidths[fn] +
|
||||
"px !important; max-width: none !important; min-width: 0 !important; }"
|
||||
);
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
resizeStyle.textContent = rules.join("\n");
|
||||
document.head.appendChild(resizeStyle);
|
||||
|
||||
// Set data-jey-grid on grids as soon as they appear so CSS rules apply immediately
|
||||
if (earlyGridKeys.length) {
|
||||
if (earlyGridKeys.length || earlyListDoctypes.length) {
|
||||
var earlyObs = new MutationObserver(function () {
|
||||
earlyGridKeys.forEach(function (key) {
|
||||
// key = "Sales Invoice:items" → fieldname = "items"
|
||||
|
|
@ -58,6 +73,18 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
if (earlyListDoctypes.length) {
|
||||
// Stamp data-jey-list on any .list-view whose route matches a saved doctype
|
||||
var route = (window.frappe && frappe.get_route) ? frappe.get_route() : null;
|
||||
var routeDoctype = (route && route[0] === "List") ? route[1] : null;
|
||||
if (routeDoctype && earlyListDoctypes.indexOf(routeDoctype) !== -1) {
|
||||
document.querySelectorAll(".list-view").forEach(function (lv) {
|
||||
if (lv.dataset.jeyList !== routeDoctype) {
|
||||
lv.dataset.jeyList = routeDoctype;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
earlyObs.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
|
@ -1344,6 +1371,517 @@
|
|||
dragObserver.observe(document.body, { childList: true, subtree: true });
|
||||
})();
|
||||
|
||||
/* ==========================================================================
|
||||
List View Column Resize + Reorder
|
||||
Drag the right edge of a list header to resize; drag the header body to
|
||||
reorder. Widths + order persist in localStorage per doctype:
|
||||
jey-list-col-widths:<Doctype> { fieldname: pxWidth, ... }
|
||||
jey-list-col-order:<Doctype> [fieldname, fieldname, ...]
|
||||
========================================================================== */
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var MIN_COL_WIDTH = 60;
|
||||
var WIDTH_PREFIX = "jey-list-col-widths:";
|
||||
var ORDER_PREFIX = "jey-list-col-order:";
|
||||
var DRAG_THRESHOLD = 5;
|
||||
|
||||
// Classnames that appear on .list-row-col but are NOT fieldnames.
|
||||
var NON_FIELD_CLASSES = {
|
||||
"list-row-col": 1, "ellipsis": 1, "hidden-xs": 1, "text-right": 1,
|
||||
"list-subject": 1, "level": 1, "level-item": 1,
|
||||
"tag-col": 1, "hide": 1, "bold": 1, "seen": 1, "notseen": 1,
|
||||
"inner-group-button": 1
|
||||
};
|
||||
|
||||
var styleEl = document.getElementById("jey-col-resize-styles");
|
||||
// { "Sales Invoice": { customer: 220, ... }, ... }
|
||||
var allListWidths = {};
|
||||
// Tracks doctypes already initialised from localStorage during this session
|
||||
var loaded = {};
|
||||
|
||||
function escapeClass(fn) {
|
||||
return (window.CSS && CSS.escape) ? CSS.escape(fn) : fn.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
||||
}
|
||||
|
||||
// Rebuild only the list-scoped portion of the shared stylesheet.
|
||||
// Grid rules live inside their own <style> section? No — they share this element.
|
||||
// To avoid trashing grid rules, we keep a dedicated second stylesheet for lists.
|
||||
var listStyleEl = document.getElementById("jey-list-col-styles");
|
||||
if (!listStyleEl) {
|
||||
listStyleEl = document.createElement("style");
|
||||
listStyleEl.id = "jey-list-col-styles";
|
||||
document.head.appendChild(listStyleEl);
|
||||
}
|
||||
|
||||
// FOUC-only stylesheet: used before JS has had a chance to stamp inline
|
||||
// styles on each cell. Targets header cells via their native fieldname
|
||||
// class (Frappe sets that from the first paint). Runtime width locking
|
||||
// happens through applyWidthsToCells(), which writes inline !important
|
||||
// styles by position — that's the one that has to win against Frappe's
|
||||
// apply_column_widths() and the inherent flex weirdness of .level-left.
|
||||
function rebuildListStylesheet() {
|
||||
var rules = [];
|
||||
Object.keys(allListWidths).forEach(function (doctype) {
|
||||
var widths = allListWidths[doctype];
|
||||
Object.keys(widths).forEach(function (fn) {
|
||||
var w = widths[fn];
|
||||
var esc = escapeClass(fn);
|
||||
rules.push(
|
||||
'.list-view[data-jey-list="' + doctype + '"] .list-row-col.' + esc +
|
||||
" { flex: 0 0 " + w + "px !important; width: " + w +
|
||||
"px !important; max-width: none !important; min-width: 0 !important; }"
|
||||
);
|
||||
});
|
||||
});
|
||||
listStyleEl.textContent = rules.join("\n");
|
||||
}
|
||||
|
||||
function setCellWidthStyle(cell, w) {
|
||||
cell.style.setProperty("flex", "0 0 " + w + "px", "important");
|
||||
cell.style.setProperty("width", w + "px", "important");
|
||||
cell.style.setProperty("max-width", "none", "important");
|
||||
cell.style.setProperty("min-width", "0", "important");
|
||||
}
|
||||
|
||||
// Freeze EVERY cell (header + data rows) at a fixed width. Saved width
|
||||
// wins if we have one for the column's fieldname; otherwise we use the
|
||||
// cell's current rendered width.
|
||||
//
|
||||
// Freezing unconditionally — even columns we can't identify a fieldname
|
||||
// for (Status without a docfield, Button columns, etc.) — is essential:
|
||||
// otherwise one flex-shrinkable column absorbs all of another column's
|
||||
// growth and you get "right edge doesn't move, left edge slides left".
|
||||
function applyWidthsToCells(listview) {
|
||||
var root = listview.$result && listview.$result[0];
|
||||
if (!root) return;
|
||||
var saved = allListWidths[listview.doctype] || {};
|
||||
var headerCells = root.querySelectorAll(".list-row-head .level-left > .list-row-col");
|
||||
if (!headerCells.length) return;
|
||||
|
||||
// Pass 1: decide each column's target width.
|
||||
var widths = [];
|
||||
var fieldOrder = [];
|
||||
headerCells.forEach(function (col) {
|
||||
var fn = fieldnameOf(col);
|
||||
fieldOrder.push(fn);
|
||||
var w;
|
||||
if (fn && saved[fn] != null) {
|
||||
w = saved[fn];
|
||||
} else {
|
||||
w = Math.round(col.getBoundingClientRect().width);
|
||||
}
|
||||
widths.push(w);
|
||||
});
|
||||
|
||||
// Pass 2: apply. Header first.
|
||||
headerCells.forEach(function (col, idx) {
|
||||
if (widths[idx] > 0) setCellWidthStyle(col, widths[idx]);
|
||||
});
|
||||
|
||||
// Then every data row, matched to header by position.
|
||||
root.querySelectorAll(".list-row:not(.list-row-head) .level-left").forEach(function (rowLeft) {
|
||||
var cells = rowLeft.querySelectorAll(":scope > .list-row-col");
|
||||
cells.forEach(function (cell, idx) {
|
||||
if (widths[idx] > 0) setCellWidthStyle(cell, widths[idx]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// During drag, only the active column's cells change. Update just those —
|
||||
// same O(rows) cost as the full apply, but narrower scope.
|
||||
function applyActiveColumnWidth(listview, fn, w) {
|
||||
if (!fn) return;
|
||||
var root = listview.$result && listview.$result[0];
|
||||
if (!root) return;
|
||||
var headerCells = root.querySelectorAll(".list-row-head .level-left > .list-row-col");
|
||||
var activeIdx = -1;
|
||||
for (var i = 0; i < headerCells.length; i++) {
|
||||
if (fieldnameOf(headerCells[i]) === fn) { activeIdx = i; break; }
|
||||
}
|
||||
if (activeIdx === -1) return;
|
||||
|
||||
setCellWidthStyle(headerCells[activeIdx], w);
|
||||
root.querySelectorAll(".list-row:not(.list-row-head) .level-left").forEach(function (rowLeft) {
|
||||
var cells = rowLeft.querySelectorAll(":scope > .list-row-col");
|
||||
if (cells[activeIdx]) setCellWidthStyle(cells[activeIdx], w);
|
||||
});
|
||||
}
|
||||
|
||||
function loadWidths(doctype) {
|
||||
if (loaded[doctype]) return allListWidths[doctype] || null;
|
||||
loaded[doctype] = true;
|
||||
try {
|
||||
var raw = localStorage.getItem(WIDTH_PREFIX + doctype);
|
||||
if (raw) {
|
||||
var parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
allListWidths[doctype] = parsed;
|
||||
rebuildListStylesheet();
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveWidths(doctype) {
|
||||
if (!doctype || !allListWidths[doctype]) return;
|
||||
localStorage.setItem(WIDTH_PREFIX + doctype, JSON.stringify(allListWidths[doctype]));
|
||||
}
|
||||
|
||||
function loadOrder(doctype) {
|
||||
try {
|
||||
var raw = localStorage.getItem(ORDER_PREFIX + doctype);
|
||||
if (raw) {
|
||||
var parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveOrder(doctype, order) {
|
||||
localStorage.setItem(ORDER_PREFIX + doctype, JSON.stringify(order));
|
||||
}
|
||||
|
||||
function fieldnameOf(colEl) {
|
||||
// Prefer the stamped attribute (covers Status/Tag/etc cells that
|
||||
// ship without a fieldname class).
|
||||
if (colEl.dataset && colEl.dataset.jeyField) return colEl.dataset.jeyField;
|
||||
// Headers always carry <span data-sort-by="<fieldname>">.
|
||||
var sortSpan = colEl.querySelector && colEl.querySelector("[data-sort-by]");
|
||||
if (sortSpan) {
|
||||
var fn = sortSpan.getAttribute("data-sort-by");
|
||||
if (fn) return fn;
|
||||
}
|
||||
// Fallback: first non-structural class.
|
||||
var classes = (colEl.className || "").split(/\s+/);
|
||||
for (var i = 0; i < classes.length; i++) {
|
||||
var c = classes[i];
|
||||
if (c && !NON_FIELD_CLASSES[c] && c.indexOf("jey-") !== 0) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Stamp data-jey-field on every column cell (header + data rows) so the
|
||||
// rest of the module can target columns uniformly. Frappe ships some
|
||||
// built-in columns (Tag, Status, Button, …) without a fieldname class
|
||||
// or a data-sort-by span — we assign a synthetic position-based key
|
||||
// so those still participate in resize and reorder.
|
||||
function stampFieldAttributes(listview) {
|
||||
var $result = listview.$result;
|
||||
if (!$result || !$result.length) return;
|
||||
var root = $result[0];
|
||||
|
||||
var headerLeft = root.querySelector(".list-row-head .level-left");
|
||||
if (!headerLeft) return;
|
||||
|
||||
var headerCells = headerLeft.querySelectorAll(":scope > .list-row-col");
|
||||
var fieldOrder = [];
|
||||
headerCells.forEach(function (col, idx) {
|
||||
var fn = fieldnameOf(col);
|
||||
if (!fn) fn = "__jey_col_" + idx + "__";
|
||||
col.dataset.jeyField = fn;
|
||||
fieldOrder.push(fn);
|
||||
});
|
||||
|
||||
root.querySelectorAll(".list-row:not(.list-row-head) .level-left").forEach(function (rowLeft) {
|
||||
var cells = rowLeft.querySelectorAll(":scope > .list-row-col");
|
||||
cells.forEach(function (cell, idx) {
|
||||
var fn = fieldOrder[idx];
|
||||
if (fn) cell.dataset.jeyField = fn;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isSubject(colEl) {
|
||||
return colEl.classList.contains("list-subject");
|
||||
}
|
||||
|
||||
function getListContainer(listview) {
|
||||
var root = listview && listview.$result ? listview.$result[0] : null;
|
||||
if (!root) return null;
|
||||
return root.closest(".list-view");
|
||||
}
|
||||
|
||||
function applyOrder(listview) {
|
||||
var doctype = listview.doctype;
|
||||
var order = loadOrder(doctype);
|
||||
if (!order || !order.length) return;
|
||||
|
||||
var $result = listview.$result;
|
||||
if (!$result || !$result.length) return;
|
||||
|
||||
// Header first, then every data row.
|
||||
var groups = [];
|
||||
var headerLeft = $result[0].querySelector(".list-row-head .level-left");
|
||||
if (headerLeft) groups.push(headerLeft);
|
||||
$result[0].querySelectorAll(".list-row:not(.list-row-head) .level-left").forEach(function (g) {
|
||||
groups.push(g);
|
||||
});
|
||||
|
||||
groups.forEach(function (group) {
|
||||
var byField = {};
|
||||
group.querySelectorAll(":scope > .list-row-col").forEach(function (col) {
|
||||
var fn = fieldnameOf(col);
|
||||
if (fn) byField[fn] = col;
|
||||
});
|
||||
// Append in saved order; subject column stays where it is (skipped).
|
||||
order.forEach(function (fn) {
|
||||
var node = byField[fn];
|
||||
if (node && !isSubject(node)) group.appendChild(node);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildCurrentOrder(listview) {
|
||||
var $result = listview.$result;
|
||||
if (!$result || !$result.length) return [];
|
||||
var header = $result[0].querySelector(".list-row-head .level-left");
|
||||
if (!header) return [];
|
||||
var order = [];
|
||||
header.querySelectorAll(":scope > .list-row-col").forEach(function (col) {
|
||||
if (isSubject(col)) return;
|
||||
var fn = fieldnameOf(col);
|
||||
if (fn) order.push(fn);
|
||||
});
|
||||
return order;
|
||||
}
|
||||
|
||||
function injectResizeHandles(listview) {
|
||||
var $result = listview.$result;
|
||||
if (!$result || !$result.length) return;
|
||||
var headerCols = $result[0].querySelectorAll(".list-row-head .level-left > .list-row-col");
|
||||
var doctype = listview.doctype;
|
||||
|
||||
headerCols.forEach(function (col) {
|
||||
// Subject gets a resize handle too (right edge, well away from
|
||||
// the checkbox on the left) — its content often outruns Frappe's
|
||||
// auto-calculated width and users need to widen it.
|
||||
if (col.querySelector(".jey-col-resize")) return;
|
||||
|
||||
var handle = document.createElement("div");
|
||||
handle.className = "jey-col-resize";
|
||||
col.appendChild(handle);
|
||||
|
||||
handle.addEventListener("mousedown", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
startResize(listview, col, e.clientX);
|
||||
});
|
||||
|
||||
handle.addEventListener("dblclick", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
autoFit(listview, col);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function autoFit(listview, headerCol) {
|
||||
var doctype = listview.doctype;
|
||||
var fn = fieldnameOf(headerCol);
|
||||
if (!fn) return;
|
||||
var label = headerCol.querySelector("span, a, .list-subject");
|
||||
var text = label ? label.textContent.trim() : fn;
|
||||
var canvas = document.createElement("canvas");
|
||||
var ctx = canvas.getContext("2d");
|
||||
var style = getComputedStyle(headerCol);
|
||||
ctx.font = style.fontWeight + " " + style.fontSize + " " + style.fontFamily;
|
||||
var w = Math.max(MIN_COL_WIDTH, Math.ceil(ctx.measureText(text).width) + 40);
|
||||
if (!allListWidths[doctype]) allListWidths[doctype] = {};
|
||||
allListWidths[doctype][fn] = w;
|
||||
applyActiveColumnWidth(listview, fn, w);
|
||||
rebuildListStylesheet();
|
||||
saveWidths(doctype);
|
||||
}
|
||||
|
||||
function startResize(listview, headerCol, startX) {
|
||||
var doctype = listview.doctype;
|
||||
var fn = fieldnameOf(headerCol);
|
||||
if (!fn) return;
|
||||
|
||||
var startWidth = headerCol.getBoundingClientRect().width;
|
||||
if (!allListWidths[doctype]) allListWidths[doctype] = {};
|
||||
var widths = allListWidths[doctype];
|
||||
|
||||
// Freeze every header column at its current rendered width so only the
|
||||
// dragged one moves. Includes .list-subject — it has flex-shrink:1
|
||||
// from list.scss and would otherwise compress while another column
|
||||
// grows, visually anchoring the dragged column's right edge.
|
||||
var cols = listview.$result[0].querySelectorAll(".list-row-head .level-left > .list-row-col");
|
||||
cols.forEach(function (c) {
|
||||
var f = fieldnameOf(c);
|
||||
if (f && !widths[f]) {
|
||||
widths[f] = Math.round(c.getBoundingClientRect().width);
|
||||
}
|
||||
});
|
||||
applyWidthsToCells(listview);
|
||||
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
|
||||
function onMove(e) {
|
||||
var w = Math.max(MIN_COL_WIDTH, Math.round(startWidth + (e.clientX - startX)));
|
||||
widths[fn] = w;
|
||||
applyActiveColumnWidth(listview, fn, w);
|
||||
}
|
||||
function onUp() {
|
||||
document.removeEventListener("mousemove", onMove);
|
||||
document.removeEventListener("mouseup", onUp);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
rebuildListStylesheet();
|
||||
saveWidths(doctype);
|
||||
}
|
||||
document.addEventListener("mousemove", onMove);
|
||||
document.addEventListener("mouseup", onUp);
|
||||
}
|
||||
|
||||
/* -------- Drag to reorder -------- */
|
||||
|
||||
var dragState = null;
|
||||
|
||||
function bindReorder(listview) {
|
||||
var $result = listview.$result;
|
||||
if (!$result || !$result.length) return;
|
||||
var headerCols = $result[0].querySelectorAll(".list-row-head .level-left > .list-row-col");
|
||||
|
||||
headerCols.forEach(function (col) {
|
||||
if (isSubject(col)) return;
|
||||
if (col.dataset.jeyDraggable) return;
|
||||
col.dataset.jeyDraggable = "1";
|
||||
|
||||
col.addEventListener("mousedown", function (e) {
|
||||
if (e.target.classList.contains("jey-col-resize")) return;
|
||||
if (e.button !== 0) return;
|
||||
var fn = fieldnameOf(col);
|
||||
if (!fn) return;
|
||||
dragState = {
|
||||
listview: listview,
|
||||
fromField: fn,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
dragging: false,
|
||||
toField: null
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", function (e) {
|
||||
if (!dragState) return;
|
||||
|
||||
if (!dragState.dragging) {
|
||||
var dx = Math.abs(e.clientX - dragState.startX);
|
||||
var dy = Math.abs(e.clientY - dragState.startY);
|
||||
if (dx < DRAG_THRESHOLD && dy < DRAG_THRESHOLD) return;
|
||||
dragState.dragging = true;
|
||||
document.body.style.cursor = "grabbing";
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
var root = dragState.listview.$result[0];
|
||||
root.querySelectorAll(".list-row-head .jey-drop-target").forEach(function (c) {
|
||||
c.classList.remove("jey-drop-target", "jey-drop-left", "jey-drop-right");
|
||||
});
|
||||
|
||||
var target = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!target) return;
|
||||
var col = target.closest(".list-row-head .level-left > .list-row-col");
|
||||
if (!col || !root.contains(col) || isSubject(col)) {
|
||||
dragState.toField = null;
|
||||
return;
|
||||
}
|
||||
var fn = fieldnameOf(col);
|
||||
if (!fn || fn === dragState.fromField) {
|
||||
dragState.toField = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var headerCols = Array.from(root.querySelectorAll(".list-row-head .level-left > .list-row-col"));
|
||||
var fi = headerCols.findIndex(function (c) { return fieldnameOf(c) === dragState.fromField; });
|
||||
var ti = headerCols.indexOf(col);
|
||||
col.classList.add("jey-drop-target");
|
||||
col.classList.toggle("jey-drop-right", fi < ti);
|
||||
col.classList.toggle("jey-drop-left", fi >= ti);
|
||||
dragState.toField = fn;
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", function () {
|
||||
if (!dragState) return;
|
||||
var state = dragState;
|
||||
dragState = null;
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
|
||||
var root = state.listview.$result ? state.listview.$result[0] : null;
|
||||
if (root) {
|
||||
root.querySelectorAll(".list-row-head .jey-drop-target").forEach(function (c) {
|
||||
c.classList.remove("jey-drop-target", "jey-drop-left", "jey-drop-right");
|
||||
});
|
||||
}
|
||||
|
||||
if (!state.dragging || !state.toField) return;
|
||||
|
||||
var order = buildCurrentOrder(state.listview);
|
||||
var fromIdx = order.indexOf(state.fromField);
|
||||
var toIdx = order.indexOf(state.toField);
|
||||
if (fromIdx === -1 || toIdx === -1 || fromIdx === toIdx) return;
|
||||
|
||||
var draggingRight = fromIdx < toIdx;
|
||||
order.splice(fromIdx, 1);
|
||||
var newToIdx = order.indexOf(state.toField);
|
||||
if (newToIdx === -1) return;
|
||||
order.splice(draggingRight ? newToIdx + 1 : newToIdx, 0, state.fromField);
|
||||
|
||||
saveOrder(state.listview.doctype, order);
|
||||
applyOrder(state.listview);
|
||||
});
|
||||
|
||||
/* -------- Wire into ListView render pipeline -------- */
|
||||
|
||||
function onListRender(listview) {
|
||||
if (!listview || listview.view_name !== "List") return;
|
||||
|
||||
var container = getListContainer(listview);
|
||||
if (container) container.dataset.jeyList = listview.doctype;
|
||||
|
||||
loadWidths(listview.doctype);
|
||||
stampFieldAttributes(listview);
|
||||
applyOrder(listview);
|
||||
applyWidthsToCells(listview);
|
||||
injectResizeHandles(listview);
|
||||
bindReorder(listview);
|
||||
}
|
||||
|
||||
function wrapRenderList() {
|
||||
if (!window.frappe || !frappe.views || !frappe.views.ListView) return false;
|
||||
var proto = frappe.views.ListView.prototype;
|
||||
if (proto.__jeyRenderListWrapped) return true;
|
||||
proto.__jeyRenderListWrapped = true;
|
||||
|
||||
var originalRender = proto.render_list;
|
||||
proto.render_list = function () {
|
||||
var result = originalRender.apply(this, arguments);
|
||||
try { onListRender(this); } catch (e) { /* noop */ }
|
||||
return result;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
// ListView is only loaded when the user first opens a list — poll briefly.
|
||||
if (!wrapRenderList()) {
|
||||
var attempts = 0;
|
||||
var iv = setInterval(function () {
|
||||
attempts++;
|
||||
if (wrapRenderList() || attempts > 100) clearInterval(iv);
|
||||
}, 200);
|
||||
}
|
||||
})();
|
||||
|
||||
/* ==========================================================================
|
||||
List View — Override SVG sprite icons
|
||||
Replaces <symbol> content in Frappe's SVG sprite sheets so every
|
||||
|
|
|
|||
Loading…
Reference in New Issue