Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- An app can now be dropped between two folders, on the top or bottom edge of a
folder row.

- Color fields now accept a hex code typed without its leading `#`, and say so
when a value is not a color.

Expand Down
2 changes: 1 addition & 1 deletion ui/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,6 @@ <h1 class="sec-title" id="ev-title">General</h1>
</div></div>
</div>

<script type="module" src="/js/admin.js?v=547e3870"></script>
<script type="module" src="/js/admin.js?v=a9e56ab5"></script>
</body>
</html>
15 changes: 15 additions & 0 deletions ui/js/admin-drag-logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ export function canJoinFolder(type) {
return type === 'app';
}

/* Share of a folder row's height at each edge that places the dragged row next
to the folder instead of inside it. */
export const FOLDER_EDGE = 0.25;

/** Which part of a folder row the pointer is over.
@param {number} y pointer position
@param {{ top: number, height: number }} rect the row's box
@returns {'above' | 'into' | 'below'} */
export function folderRowZone(y, rect) {
const edge = rect.height * FOLDER_EDGE;
if (y < rect.top + edge) return 'above';
if (y > rect.top + rect.height - edge) return 'below';
return 'into';
}

export function dropTargetKind({ srcType, targetIsFolder = false, indent = false }) {
if (canJoinFolder(srcType) && (targetIsFolder || indent)) return 'into-folder';
return 'reorder';
Expand Down
23 changes: 15 additions & 8 deletions ui/js/admin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { buildAppForm, buildFolderForm, serializeKvRows } from '/js/admin-app-form.js?v=fde6479c';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=76be763e';
import { applyDrop, canJoinFolder } from '/js/admin-drag-logic.js?v=8fcf583a';
import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=ebe3e806';
import { reorderItems, resolveAdminSection } from '/js/admin-logic.js?v=f3f87abf';
import {
buildAppItem,
Expand Down Expand Up @@ -363,10 +363,11 @@ function mkRow(item, idx, { indent = false, childIdx = null, folderId = null } =
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
clearDragClasses();
const rect = row.getBoundingClientRect();
if (row.dataset.isFolder && canJoinFolder(_dragType)) {
row.classList.add('drag-into');
const zone = folderRowZone(e.clientY, rect);
row.classList.add(zone === 'into' ? 'drag-into' : zone === 'above' ? 'drag-above' : 'drag-below');
} else {
const rect = row.getBoundingClientRect();
row.classList.add(e.clientY < rect.top + rect.height / 2 ? 'drag-above' : 'drag-below');
}
});
Expand All @@ -377,6 +378,7 @@ function mkRow(item, idx, { indent = false, childIdx = null, folderId = null } =
row.addEventListener('drop', e => {
e.preventDefault();
const dropAbove = row.classList.contains('drag-above');
const dropInto = row.classList.contains('drag-into');
clearDragClasses();
const raw = e.dataTransfer.getData('text/plain');
if (!raw) return;
Expand All @@ -388,7 +390,7 @@ function mkRow(item, idx, { indent = false, childIdx = null, folderId = null } =
...drop,
targetId: item.id,
targetFolderId: folderId,
targetIsFolder: item.type === 'folder',
targetIsFolder: item.type === 'folder' && dropInto,
indent,
childIdx,
dropAbove,
Expand Down Expand Up @@ -431,6 +433,7 @@ function wireTouchDrag(row, handle, { indent, folderId }) {
const offY = e.clientY - startRect.top;
let hovered = null,
dropAbove = false,
dropInto = false,
scrollTimer = null;
const scroller = scrollParent(row);

Expand All @@ -443,10 +446,14 @@ function wireTouchDrag(row, handle, { indent, folderId }) {
const tr = /** @type {HTMLElement} */ (under && under.closest('.drow'));
if (!tr || tr === row || tr === ghost) return;
hovered = tr;
const r = tr.getBoundingClientRect();
if (tr.dataset.isFolder && canJoinFolder(itemType(srcId))) {
tr.classList.add('drag-into');
const zone = folderRowZone(y, r);
dropInto = zone === 'into';
dropAbove = zone === 'above';
tr.classList.add(dropInto ? 'drag-into' : dropAbove ? 'drag-above' : 'drag-below');
} else {
const r = tr.getBoundingClientRect();
dropInto = false;
dropAbove = y < r.top + r.height / 2;
tr.classList.add(dropAbove ? 'drag-above' : 'drag-below');
}
Expand Down Expand Up @@ -485,14 +492,14 @@ function wireTouchDrag(row, handle, { indent, folderId }) {
const tr = hovered;
end();
if (!tr) return;
const into = tr.dataset.isFolder && canJoinFolder(itemType(srcId));
const into = !!tr.dataset.isFolder && canJoinFolder(itemType(srcId)) && dropInto;
const before = snapshotItems(state.items);
const drop = applyDrop(state.items, {
srcId,
srcFolderId: indent ? folderId : null,
targetId: tr.dataset.itemId,
targetFolderId: tr.dataset.folderId || null,
targetIsFolder: !!tr.dataset.isFolder,
targetIsFolder: into,
indent: !!tr.dataset.indent,
childIdx: tr.dataset.childIdx != null ? Number(tr.dataset.childIdx) : null,
dropAbove: into ? false : dropAbove,
Expand Down
29 changes: 28 additions & 1 deletion ui/test/admin-drag-logic.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { canJoinFolder, dropTargetKind, applyDrop } from '../js/admin-drag-logic.js';
import { canJoinFolder, dropTargetKind, applyDrop, folderRowZone } from '../js/admin-drag-logic.js';

/* a, b, w top level; f is a folder holding c. */
const model = () => [
Expand Down Expand Up @@ -171,3 +171,30 @@ test('dropping an item on itself changes nothing', () => {
);
assert.deepEqual(order(items), ['a', 'b', 'f', 'c', 'w']);
});

const rect = { top: 100, height: 40 };

test('the edges of a folder row place a row next to it', () => {
assert.equal(folderRowZone(101, rect), 'above');
assert.equal(folderRowZone(139, rect), 'below');
});

test('the middle of a folder row still drops into the folder', () => {
for (const y of [111, 120, 129]) assert.equal(folderRowZone(y, rect), 'into');
});

test('a drop on a folder edge reorders instead of joining the folder', () => {
const items = model();
applyDrop(items, {
srcId: 'a',
srcFolderId: null,
targetId: 'f',
targetFolderId: null,
targetIsFolder: false,
indent: false,
childIdx: null,
dropAbove: true,
});
assert.deepEqual(folder(items, 'f'), ['c']);
assert.deepEqual(order(items), ['b', 'a', 'f', 'c', 'w']);
});