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.