PageSourceSearch

https://geoconfirmed.org/Components/SortableList.emlhiha34z.razor.js

js geoconfirmed.org collected 2026-09-28 11:04:34 UTC 6,895 bytes, 147 lines download raw bytes

1import Sortable from '../js/sortable.esm.js';
2
3// Live pointer position, used by the "drop onto external zone" feature. SortableJS's onEnd
4// event.originalEvent is unreliable under forceFallback (it can arrive without usable pointer
5// coordinates), so we track the pointer from the document ourselves and use it as the drop
6// point. Tracking is scoped to a single drag (attached on start, detached on end) so no
7// document listeners linger app-wide after the drag.
8let lastPointer = null;
9
10// While a zone-drop drag is active: the drop-zone selector, the zone element currently under
11// the pointer (for the live "drop here" highlight), and the id of the last zone the pointer was
12// over. onEnd uses this committed id rather than re-hit-testing at drop time — at drop the ghost
13// and the drop-finalize reflow can shift things so a fresh elementsFromPoint misses the zone,
14// whereas the per-move hit-test (which drives the highlight the user sees) is reliable.
15let activeZoneSelector = null;
16let highlightedZone = null;
17let committedZoneId = null;
18const ZONE_ACTIVE_CLASS = 'sortable-dropzone-active';
19
20function trackPointer(e) {
21    const p = (e.touches && e.touches[0]) || (e.changedTouches && e.changedTouches[0]) || e;
22    if (p && typeof p.clientX === 'number') {
23        lastPointer = { x: p.clientX, y: p.clientY };
24        // Only (re)evaluate the zone on movement. The pointerup/end that finalizes the drop must
25        // not re-run the hit-test — by then the layout can have shifted — so the zone committed on
26        // the last move (the one shown as highlighted) is what the drop uses.
27        if (e.type.indexOf('move') !== -1) updateZoneHighlight();
28    }
29}
30
31// Capture phase so we record the position before SortableJS's own (bubble-phase) mouseup
32// handler fires onEnd.
33const POINTER_EVENTS = ['pointermove', 'pointerup', 'mousemove', 'mouseup', 'touchmove', 'touchend'];
34function startPointerTracking() {
35    POINTER_EVENTS.forEach(t => document.addEventListener(t, trackPointer, true));
36}
37function stopPointerTracking() {
38    POINTER_EVENTS.forEach(t => document.removeEventListener(t, trackPointer, true));
39}
40
41// Re-evaluate the zone under the pointer: commit its id (or clear it) and move the "drop here"
42// highlight to it.
43function updateZoneHighlight() {
44    if (!activeZoneSelector) return;
45    const zone = zoneElementAtPoint(activeZoneSelector);
46    committedZoneId = zone ? zoneId(zone, activeZoneSelector) : null;
47    if (zone === highlightedZone) return;
48    if (highlightedZone) highlightedZone.classList.remove(ZONE_ACTIVE_CLASS);
49    highlightedZone = zone;
50    if (highlightedZone) highlightedZone.classList.add(ZONE_ACTIVE_CLASS);
51}
52
53function clearZoneHighlight() {
54    if (highlightedZone) highlightedZone.classList.remove(ZONE_ACTIVE_CLASS);
55    highlightedZone = null;
56    activeZoneSelector = null;
57    committedZoneId = null;
58}
59
60export function init(id, group, pull, put, sort, handle, filter, component, forceFallback, dropZoneSelector) {
61    const options = {
62        animation: 200,
63        group: {
64            name: group,
65            pull: pull || true,
66            put: put
67        },
68        filter: filter || undefined,
69        preventOnFilter: false,
70        sort: sort,
71        forceFallback: forceFallback,
72        // Append the drag ghost to <body>, not the dragged element's container. Otherwise the
73        // ghost lands inside the CSS-multicolumn board and reflows the masonry mid-drag, shifting
74        // the drop target (a card header) out from under the pointer — so the drop-zone hit-test
75        // in onEnd would miss it and a cross-card drop would silently do nothing.
76        fallbackOnBody: true,
77        handle: handle || undefined,
78    };
79
80    if (dropZoneSelector) {
81        // This list can also drop its items onto an *external* zone (e.g. a Toolkit link
82        // dropped onto a tab). Everything is resolved in onEnd so a reorder and a zone-drop
83        // never both fire. The list is single (no cross-list group), so from === to.
84        options.onStart = () => {
85            activeZoneSelector = dropZoneSelector;
86            committedZoneId = null;
87            startPointerTracking();
88        };
89        options.onEnd = (event) => {
90            const droppedZoneId = committedZoneId; // the zone under the pointer on the last move
91            clearZoneHighlight();
92            stopPointerTracking();
93
94            // Revert SortableJS's DOM change — Blazor owns the list's rendering.
95            event.item.remove();
96            const ref = event.from.children[event.oldIndex] || null;
97            event.from.insertBefore(event.item, ref);
98
99            if (droppedZoneId) {
100                component.invokeMethodAsync('OnDropOutsideJS', event.oldDraggableIndex, droppedZoneId);
101            } else if (event.oldDraggableIndex !== event.newDraggableIndex) {
102                component.invokeMethodAsync('OnUpdateJS', event.oldDraggableIndex, event.newDraggableIndex);
103            }
104        };
105    } else {
106        options.onUpdate = (event) => {
107            event.item.remove();
108            event.to.insertBefore(event.item, event.to.children[event.oldIndex]);
109            component.invokeMethodAsync('OnUpdateJS', event.oldDraggableIndex, event.newDraggableIndex);
110        };
111        options.onRemove = (event) => {
112            if (event.pullMode === 'clone') {
113                event.clone.remove();
114            }
115
116            event.item.remove();
117            event.from.insertBefore(event.item, event.from.children[event.oldIndex]);
118            component.invokeMethodAsync('OnRemoveJS', event.oldDraggableIndex, event.newDraggableIndex, event.from.id, event.to.id);
119        };
120    }
121
122    // The element can be gone by the time this queued interop call runs (the component was
123    // disposed by a re-render between .NET dispatching init and JS executing it, e.g. the
124    // Toolkit board re-slicing its columns). Sortable(null) would throw — just skip.
125    const el = document.getElementById(id);
126    if (el) new Sortable(el, options);
127}
128
129// The top-most element under the live pointer that matches `selector`, or null. Uses
130// elementsFromPoint so the drag ghost overlay (which never matches the selector) is skipped.
131function zoneElementAtPoint(selector) {
132    if (!lastPointer) return null;
133
134    for (const el of document.elementsFromPoint(lastPointer.x, lastPointer.y)) {
135        const zone = el.closest(selector);
136        if (zone) return zone;
137    }
138    return null;
139}
140
141// The drop id carried by a zone element: the value of whichever attribute the selector matched
142// (e.g. "[data-drop-id]" → data-drop-id for a link onto a tab, "[data-card-drop-id]" for a tab
143// onto a card) so each kind of drag carries its own zone id.
144function zoneId(zone, selector) {
145    const attr = selector.match(/\[([\w-]+)\]/);
146    return attr ? zone.getAttribute(attr[1]) : zone.getAttribute('data-drop-id');
147}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.