Skip to content

Commit

Permalink
luci-base: make items of UIDynamicList drag-sortable
Browse files Browse the repository at this point in the history
Signed-off-by: Ramon Van Gorkom <[email protected]>
  • Loading branch information
Ramon00 authored and systemcrash committed Nov 27, 2024
1 parent fefb9ac commit 80f18df
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 7 deletions.
4 changes: 3 additions & 1 deletion modules/luci-base/htdocs/luci-static/resources/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -2752,7 +2752,6 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
handleDragStart: function(ev) {
if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
scope.dragState = null;
ev.preventDefault();
return false;
}

Expand All @@ -2763,6 +2762,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p

/** @private */
handleDragOver: function(ev) {
if (scope.dragState === null ) return;
var n = scope.dragState.targetNode,
r = scope.dragState.rect,
t = r.top + r.height / 2;
Expand All @@ -2783,6 +2783,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p

/** @private */
handleDragEnter: function(ev) {
if (scope.dragState === null ) return;
scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
scope.dragState.targetNode = ev.currentTarget;
},
Expand All @@ -2808,6 +2809,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p

/** @private */
handleDrop: function(ev) {
if (scope.dragState === null ) return;
var s = scope.dragState;

if (s.node && s.targetNode) {
Expand Down
103 changes: 101 additions & 2 deletions modules/luci-base/htdocs/luci-static/resources/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -2265,9 +2265,98 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
this.addItem(dl, this.values[i], label);
}

this.initDragAndDrop(dl);

return this.bind(dl);
},

/** @private */
initDragAndDrop: function(dl) {
let draggedItem = null;
let placeholder = null;

dl.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('item')) {
draggedItem = e.target;
e.target.classList.add('dragging');
}
});

dl.addEventListener('dragend', (e) => e.target.classList.remove('dragging'));

dl.addEventListener('dragover', (e) => e.preventDefault());

dl.addEventListener('dragenter', (e) => e.target.classList.add('drag-over'));

dl.addEventListener('dragleave', (e) => e.target.classList.remove('drag-over'));

dl.addEventListener('drop', (e) => {
e.preventDefault();
e.target.classList.remove('drag-over');
const target = e.target.classList.contains('item') ? e.target : dl.querySelector('.add-item');
dl.insertBefore(draggedItem, target);
});

dl.addEventListener('click', (e) => {
if (e.target.closest('.item')) {
const span = e.target.closest('.item').querySelector('SPAN');
if (span) {
const range = document.createRange();
range.selectNodeContents(span);
const selection = window.getSelection();
if (selection.rangeCount === 0 || selection.toString().length === 0) {
selection.removeAllRanges();
selection.addRange(range);
} else selection.removeAllRanges();
}
}
});

dl.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
const target = e.target.closest('.item');
if (target) {
draggedItem = target;

placeholder = draggedItem.cloneNode(true);
placeholder.className = 'placeholder';
placeholder.style.height = `${draggedItem.offsetHeight}px`;
draggedItem.parentNode.insertBefore(placeholder, draggedItem.nextSibling);
draggedItem.classList.add('dragging')
}
});

dl.addEventListener('touchmove', (e) => {
if (draggedItem) {
const touch = e.touches[0];
const currentY = touch.clientY;

const items = Array.from(dl.querySelectorAll('.item'));
const target = items.find(item => {
const rect = item.getBoundingClientRect();
return currentY > rect.top && currentY < rect.bottom;
});

if (target && target !== draggedItem) {
const insertBefore = currentY < target.getBoundingClientRect().top + target.offsetHeight / 2;
dl.insertBefore(placeholder, insertBefore ? target : target.nextSibling);
}

e.preventDefault();
}
});

dl.addEventListener('touchend', (e) => {
if (draggedItem && placeholder) {
dl.insertBefore(draggedItem, placeholder);
draggedItem.classList.remove('dragging')
placeholder.parentNode.removeChild(placeholder);
placeholder = null;
draggedItem = null;
}
});
},

/** @private */
bind: function(dl) {
dl.addEventListener('click', L.bind(this.handleClick, this));
Expand All @@ -2287,7 +2376,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
/** @private */
addItem: function(dl, value, text, flash) {
var exists = false,
new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0, 'draggable': true }, [
E('span', {}, [ text || value ]),
E('input', {
'type': 'hidden',
Expand Down Expand Up @@ -2359,7 +2448,17 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
return;

if (item) {
this.removeItem(dl, item);
// Get bounding rectangle of the item
var rect = item.getBoundingClientRect();

// Get computed styles for the ::after pseudo-element
var afterStyles = window.getComputedStyle(item, '::after');
var afterWidth = parseFloat(afterStyles.width) || 0;

// Check if the click is within the ::after region
if (rect.right - ev.clientX <= afterWidth) {
this.removeItem(dl, item);
}
}
else if (matchesElem(ev.target, '.cbi-button-add')) {
var input = ev.target.previousElementSibling;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -624,9 +624,11 @@ select,
border-radius: 3px;
color: var(--text-color-high);
position: relative;
pointer-events: none;
pointer-events: auto; /* needed for drag-and-drop in UIDynamicList */
overflow: hidden;
word-break: break-all;
cursor: move; /* drag-and-drop */
user-select: text; /* text selection in drag-and-drop */
}

.cbi-dynlist > .item::after {
Expand All @@ -645,10 +647,35 @@ select,
pointer-events: auto;
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .item.drag-over {
border-top: 1px solid var(--text-color-highest);
}

/* Make item being dragged in UIDynamicList partially transparent*/
.cbi-dynlist > .item.dragging {
opacity: 0.5;
}

/* prevent pointer changing when over the span element in UIDynamicList */
.cbi-dynlist > .item > span {
pointer-events: none;
}

.cbi-dynlist > .add-item {
display: flex;
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .add-item > .cbi-input-text.drag-over {
border-top: 1px solid var(--text-color-highest);
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .add-item > .cbi-button-add.drag-over {
border-top: 1px solid var(--text-color-highest);
}

.cbi-dynlist > .add-item > input,
.cbi-dynlist > .add-item > button {
flex: 1 1 auto;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1378,10 +1378,12 @@ body:not(.Interfaces) .cbi-rowstyle-2:first-child {
max-width: 25rem;
margin-right: 2em;
padding: .5em .25em .25em 0;
pointer-events: none;
pointer-events: auto; /* needed for drag-and-drop in UIDynamicList */
color: #666;
border-bottom: 2px solid rgba(0, 0, 0, .26);
outline: 0;
cursor: move; /* drag-and-drop */
user-select: text; /* text selection in drag-and-drop */
}

.cbi-dynlist[name="sshkeys"] > .item {
Expand All @@ -1403,9 +1405,21 @@ body:not(.Interfaces) .cbi-rowstyle-2:first-child {
background-color: var(--red-color-high);
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .item.drag-over {
border-top: 1px solid black;
}

/* Make item being dragged in UIDynamicList partially transparent*/
.cbi-dynlist > .item.dragging {
opacity: 0.5;
}

/* prevent pointer changing when over the span element in UIDynamicList */
.cbi-dynlist > .item > span {
white-space: normal;
word-break: break-word;
pointer-events: none;
}

.cbi-dynlist > .add-item {
Expand All @@ -1415,6 +1429,16 @@ body:not(.Interfaces) .cbi-rowstyle-2:first-child {
min-width: 16rem;
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .add-item > .cbi-input-text.drag-over {
border-top: 1px solid black;
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .add-item > .cbi-button-add.drag-over {
border-top: 1px solid black;
}

.cbi-dynlist > .add-item:not([ondrop]) > input {
overflow: hidden;
width: 100%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1173,9 +1173,11 @@ textarea {
position: relative;
overflow: hidden;
transition: box-shadow .25s ease-in-out;
pointer-events: none;
pointer-events: auto; /* needed for drag-and-drop in UIDynamicList */
flex: 1 1 100%;
word-break: break-all;
cursor: move; /* drag-and-drop */
user-select: text; /* text selection in drag-and-drop */
}

.cbi-dynlist > .item::after {
Expand All @@ -1196,6 +1198,20 @@ textarea {
pointer-events: all;
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .item.drag-over {
border-top: 1px solid black;
}

/* Make item being dragged in UIDynamicList partially transparent*/
.cbi-dynlist > .item.dragging {
opacity: 0.5;
}
/* prevent pointer changing when over the span element in UIDynamicList */
.cbi-dynlist > .item > span {
pointer-events: none;
}

.cbi-dynlist[disabled] > .item::after {
pointer-events: none;
}
Expand All @@ -1209,6 +1225,16 @@ textarea {
display: flex;
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .add-item > .cbi-input-text.drag-over {
border-top: 1px solid black;
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .add-item > .cbi-button-add.drag-over {
border-top: 1px solid black;
}

.cbi-dynlist > .add-item > input {
flex: 1;
min-width: 18.5rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1340,10 +1340,12 @@ ul.cbi-tabmenu li.cbi-tab-disabled[data-errors]::after {
border: 1px outset #000;
border-radius: 3px;
position: relative;
pointer-events: none;
pointer-events: auto; /* needed for drag-and-drop in UIDynamicList */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: move; /* drag-and-drop */
user-select: text; /* text selection in drag-and-drop */
}

.cbi-dynlist > .item::after {
Expand All @@ -1364,10 +1366,35 @@ ul.cbi-tabmenu li.cbi-tab-disabled[data-errors]::after {
height: auto;
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .item.drag-over {
border-top: 1px solid red;
}

/* Make item being dragged in UIDynamicList partially transparent*/
.cbi-dynlist > .item.dragging {
opacity: 0.5;
}

/* prevent pointer changing when over the span element in UIDynamicList */
.cbi-dynlist > .item > span {
pointer-events: none;
}

.cbi-dynlist > .add-item {
display: flex;
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .add-item > .cbi-input-text.drag-over {
border-top: 1px solid re;
}

/* indication line for drag-and-drop in UIDynamicList*/
.cbi-dynlist > .add-item > .cbi-button-add.drag-over {
border-top: 1px solid red;
}

.cbi-dynlist > .add-item > input {
flex: 1 1 auto;
}
Expand Down

0 comments on commit 80f18df

Please sign in to comment.