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);
|
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
|
Sidebar — Full Width button
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
resizeStyle.id = "jey-col-resize-styles";
|
resizeStyle.id = "jey-col-resize-styles";
|
||||||
var rules = [];
|
var rules = [];
|
||||||
var earlyGridKeys = [];
|
var earlyGridKeys = [];
|
||||||
|
var earlyListDoctypes = [];
|
||||||
for (var i = 0; i < localStorage.length; i++) {
|
for (var i = 0; i < localStorage.length; i++) {
|
||||||
var k = localStorage.key(i);
|
var k = localStorage.key(i);
|
||||||
if (k.indexOf("jey-col-widths:") === 0) {
|
if (k.indexOf("jey-col-widths:") === 0) {
|
||||||
|
|
@ -37,13 +38,27 @@
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} catch (e) {}
|
} 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");
|
resizeStyle.textContent = rules.join("\n");
|
||||||
document.head.appendChild(resizeStyle);
|
document.head.appendChild(resizeStyle);
|
||||||
|
|
||||||
// Set data-jey-grid on grids as soon as they appear so CSS rules apply immediately
|
// 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 () {
|
var earlyObs = new MutationObserver(function () {
|
||||||
earlyGridKeys.forEach(function (key) {
|
earlyGridKeys.forEach(function (key) {
|
||||||
// key = "Sales Invoice:items" → fieldname = "items"
|
// 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 });
|
earlyObs.observe(document.body, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
|
|
@ -1344,6 +1371,517 @@
|
||||||
dragObserver.observe(document.body, { childList: true, subtree: true });
|
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
|
List View — Override SVG sprite icons
|
||||||
Replaces <symbol> content in Frappe's SVG sprite sheets so every
|
Replaces <symbol> content in Frappe's SVG sprite sheets so every
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue