PageSourceSearch

https://pupwife.com/scripts/desktop/citrusDesktopDnD.js

js pupwife.com collected 2026-09-30 15:04:56 UTC 22,879 bytes, 541 lines download raw bytes

1/**
2 * CitrusOS desktop drag-and-drop — folder grids, desktop shortcuts, cross-surface moves.
3 */
4(function (global) {
5    'use strict';
6
7    var DRAG_THRESHOLD = 6;
8    var activeDrag = null;
9
10    function pointerDeltaToLogical(dx, dy) {
11        if (global.CitrusBrowserZoom && typeof global.CitrusBrowserZoom.toLogicalDelta === 'function') {
12            return global.CitrusBrowserZoom.toLogicalDelta(dx, dy);
13        }
14        return { dx: dx, dy: dy };
15    }
16
17    function showToast(message) {
18        if (global.SiteContextMenu && typeof global.SiteContextMenu.showToast === 'function') {
19            global.SiteContextMenu.showToast(message);
20        }
21    }
22
23    function findIconGraphic(el) {
24        if (!el) return null;
25        if (el.closest) {
26            var nested = el.closest('.citrus-folder-icon__graphic, .citrus-desktop-icon__graphic');
27            if (nested) return nested;
28        }
29        if (el.classList && el.classList.contains('citrus-folder-icon__graphic')) return el;
30        return el.querySelector('.citrus-folder-icon__graphic') ||
31            el.querySelector('.citrus-folder-sprite-stack') ||
32            el.querySelector('.citrus-desktop-icon__img') ||
33            el.querySelector('.citrus-desktop-icon__fa') ||
34            el.querySelector('.citrus-folder-icon__img') ||
35            el.querySelector('.citrus-folder-icon__fa') ||
36            el.querySelector('img') ||
37            el.querySelector('i.fa-solid, i.fa-brands, i.fa-regular') ||
38            null;
39    }
40
41    function findIconLabel(el) {
42        if (!el) return '';
43        var labelEl = el.querySelector('.citrus-desktop-icon__label') ||
44            el.querySelector('.citrus-folder-icon__label');
45        return labelEl ? labelEl.textContent : '';
46    }
47
48    function createGhost(sourceEl, label) {
49        var graphic = findIconGraphic(sourceEl);
50        if (!graphic) return null;
51
52        var ghost = document.createElement('div');
53        ghost.className = 'citrus-dnd-ghost';
54        ghost.setAttribute('aria-hidden', 'true');
55
56        var clone = graphic.cloneNode(true);
57        clone.classList.add('citrus-dnd-ghost__icon');
58        ghost.appendChild(clone);
59
60        var caption = label || findIconLabel(sourceEl);
61        if (caption) {
62            var cap = document.createElement('span');
63            cap.className = 'citrus-dnd-ghost__label';
64            cap.textContent = caption;
65            ghost.appendChild(cap);
66        }
67
68        document.body.appendChild(ghost);
69        return ghost;
70    }
71
72    function moveGhost(ghost, clientX, clientY, copyMode) {
73        if (!ghost) return;
74        ghost.style.left = clientX + 'px';
75        ghost.style.top = clientY + 'px';
76        ghost.classList.toggle('citrus-dnd-ghost--copy', !!copyMode);
77    }
78
79    function destroyGhost() {
80        if (activeDrag && activeDrag.ghost && activeDrag.ghost.parentNode) {
81            activeDrag.ghost.parentNode.removeChild(activeDrag.ghost);
82        }
83    }
84
85    function clearDropHighlights() {
86        var desktop = document.getElementById('citrusos-desktop');
87        if (desktop) desktop.classList.remove('citrusos-desktop--drop-target');
88        var grids = document.querySelectorAll('.citrus-folder-grid--drop-target');
89        for (var i = 0; i < grids.length; i++) {
90            grids[i].classList.remove('citrus-folder-grid--drop-target');
91        }
92    }
93
94    function findDropTarget(clientX, clientY) {
95        clearDropHighlights();
96        var el = document.elementFromPoint(clientX, clientY);
97        if (!el) return null;
98
99        var desktop = el.closest('#citrusos-desktop');
100        if (desktop) {
101            var overIcon = el.closest('.citrus-desktop-icon');
102            var folderDrag = activeDrag && activeDrag.sourceType === 'folder';
103            if (!overIcon || folderDrag) {
104                desktop.classList.add('citrusos-desktop--drop-target');
105                return { type: 'desktop', el: desktop };
106            }
107        }
108
109        var grid = el.closest('[data-folder-grid]');
110        if (grid) {
111            grid.classList.add('citrus-folder-grid--drop-target');
112            var shell = grid.closest('.citrus-folder-window');
113            var folderId = shell ? shell.getAttribute('data-folder-id') : null;
114            var path = [];
115            if (folderId && global.CitrusFolderWindow && typeof global.CitrusFolderWindow.getFolderPath === 'function') {
116                path = global.CitrusFolderWindow.getFolderPath(folderId) || [];
117            }
118            return {
119                type: 'folder',
120                el: grid,
121                folderId: folderId,
122                containerId: folderId,
123                path: path
124            };
125        }
126
127        return null;
128    }
129
130    function parseDragPayload(sourceEl, sourceType) {
131        sourceType = sourceType || 'desktop';
132        if (sourceType === 'desktop') {
133            var shortcutId = sourceEl.getAttribute('data-shortcut-id');
134            var shortcuts = global.CitrusDesktopShortcuts;
135            var sc = shortcuts && shortcuts.getShortcutById ? shortcuts.getShortcutById(shortcutId) : null;
136            return {
137                kind: 'desktopShortcut',
138                shortcutId: shortcutId,
139                shortcut: sc,
140                itemKey: sc ? sc.targetKey : null
141            };
142        }
143
144        if (sourceType === 'folder') {
145            var entryType = sourceEl.getAttribute('data-entry-type');
146            var entryHref = sourceEl.getAttribute('data-entry-href');
147            var entryId = sourceEl.getAttribute('data-entry-id');
148            var userFolderId = sourceEl.getAttribute('data-user-folder-id');
149            var layout = global.CitrusFolderLayout;
150            var itemKey = '';
151            if (entryType === 'note') itemKey = layout ? layout.noteItemKey(entryId) : 'note:' + entryId;
152            else if (entryType === 'app' && entryHref) itemKey = layout ? layout.itemKeyFromHref(entryHref) : entryHref;
153            else if (userFolderId) return { kind: 'userFolderNode', userFolderId: userFolderId };
154            else if (entryType === 'folder') return { kind: 'staticFolderNode', entryId: entryId, entryType: entryType };
155            return {
156                kind: 'folderItem',
157                entryType: entryType,
158                entryHref: entryHref,
159                entryId: entryId,
160                userFolderId: userFolderId,
161                itemKey: itemKey
162            };
163        }
164
165        return null;
166    }
167
168    function desktopDropPoint(target, dropPoint) {
169        if (!dropPoint || !target || target.type !== 'desktop' || !target.el) return null;
170        var shortcuts = global.CitrusDesktopShortcuts;
171        if (!shortcuts || typeof shortcuts.clientPointToDesktop !== 'function') return null;
172        return shortcuts.clientPointToDesktop(dropPoint.clientX, dropPoint.clientY, target.el);
173    }
174
175    function handleDrop(payload, target, copyMode, sourceContext, dropPoint) {
176        if (!payload || !target) return false;
177        var shortcuts = global.CitrusDesktopShortcuts;
178        var layout = global.CitrusFolderLayout;
179        var userFolders = global.CitrusUserFolders;
180
181        if (target.type === 'desktop') {
182            if (payload.kind === 'folderItem' || payload.kind === 'staticFolderNode') {
183                var targetKey = '';
184                if (shortcuts && shortcuts.targetKeyFromFolderEntry) {
185                    var fakeEntry = {
186                        type: payload.entryType,
187                        href: payload.entryHref,
188                        id: payload.entryId,
189                        noteId: payload.entryId,
190                        userFolderId: payload.userFolderId
191                    };
192                    targetKey = shortcuts.targetKeyFromFolderEntry(fakeEntry);
193                }
194                if (!targetKey && payload.itemKey) targetKey = payload.itemKey;
195                if (shortcuts && shortcuts.addShortcut && targetKey) {
196                    var addOpts = {};
197                    var dropPt = desktopDropPoint(target, dropPoint);
198                    if (dropPt) {
199                        addOpts.x = dropPt.x;
200                        addOpts.y = dropPt.y;
201                    }
202                    shortcuts.addShortcut(targetKey, addOpts);
203                    if (global.CitrusDesktop && global.CitrusDesktop.refreshDesktopIcons) {
204                        global.CitrusDesktop.refreshDesktopIcons();
205                    }
206                    return true;
207                }
208            }
209            if (payload.kind === 'userFolderNode' && userFolders) {
210                var ufKey = 'user:' + payload.userFolderId;
211                if (shortcuts && shortcuts.addShortcut) {
212                    var ufOpts = {};
213                    var ufDropPt = desktopDropPoint(target, dropPoint);
214                    if (ufDropPt) {
215                        ufOpts.x = ufDropPt.x;
216                        ufOpts.y = ufDropPt.y;
217                    }
218                    shortcuts.addShortcut(ufKey, ufOpts);
219                    if (global.CitrusDesktop && global.CitrusDesktop.refreshDesktopIcons) {
220                        global.CitrusDesktop.refreshDesktopIcons();
221                    }
222                    return true;
223                }
224            }
225            return false;
226        }
227
228        if (target.type === 'folder') {
229            var containerId = target.containerId;
230            if (containerId && containerId.indexOf('user:') !== 0 && containerId.indexOf('user:') === -1) {
231                containerId = target.folderId;
232            }
233            var path = target.path || [];
234
235            if (payload.kind === 'desktopShortcut') {
236                if (!layout || !payload.itemKey) return false;
237                var itemKey = global.CitrusDesktopShortcuts && global.CitrusDesktopShortcuts.resolveLayoutItemKey
238                    ? global.CitrusDesktopShortcuts.resolveLayoutItemKey(payload.itemKey)
239                    : payload.itemKey;
240                if (itemKey.indexOf('user:') === 0 || !itemKey) {
241                    showToast('This item cannot be placed in a folder.');
242                    return false;
243                }
244                layout.moveItem(itemKey, containerId, path);
245                if (!copyMode && shortcuts && shortcuts.removeShortcut) {
246                    shortcuts.removeShortcut(payload.shortcutId);
247                }
248                refreshFolderViews(containerId, sourceContext);
249                if (global.CitrusDesktop && global.CitrusDesktop.refreshDesktopIcons) {
250                    global.CitrusDesktop.refreshDesktopIcons();
251                }
252                return true;
253            }
254
255            if (payload.kind === 'folderItem' && layout && payload.itemKey) {
256                layout.moveItem(payload.itemKey, containerId, path);
257                refreshFolderViews(containerId, sourceContext);
258                return true;
259            }
260
261            if (payload.kind === 'userFolderNode' && userFolders && sourceContext) {
262                var srcContainer = sourceContext.containerId;
263                var srcPath = sourceContext.path || [];
264                if (userFolders.isDescendant && containerId.indexOf('user:') === 0 &&
265                    userFolders.isDescendant(containerId.slice(5), payload.userFolderId)) {
266                    showToast('Cannot move a folder into itself.');
267                    return false;
268                }
269                if (containerId.indexOf('user:') === 0) {
270                    userFolders.moveFolderNode(payload.userFolderId, containerId.slice(5), null, []);
271                } else {
272                    userFolders.moveFolderNode(payload.userFolderId, null, containerId, path);
273                }
274                refreshFolderViews(containerId, sourceContext);
275                if (srcContainer) refreshFolderViews(srcContainer, sourceContext);
276                return true;
277            }
278        }
279
280        return false;
281    }
282
283    function refreshFolderViews(containerId, sourceContext) {
284        if (!global.CitrusFolderWindow || !global.CitrusFolderWindow.refreshOpenFolders) return;
285        global.CitrusFolderWindow.refreshOpenFolders();
286    }
287
288    function collectDesktopGroupDrag(leaderBtn) {
289        var leaderId = leaderBtn.getAttribute('data-shortcut-id');
290        var sel = global.CitrusDesktopSelection;
291        var desktop = document.getElementById('citrusos-desktop');
292        if (!desktop || !leaderId) return [{ el: leaderBtn, id: leaderId }];
293
294        if (sel && sel.getSelectedCount() > 1 && sel.isSelected(leaderId)) {
295            var ids = sel.getSelectedIds();
296            var group = [];
297            for (var i = 0; i < ids.length; i++) {
298                var el = desktop.querySelector('.citrus-desktop-icon[data-shortcut-id="' + ids[i] + '"]');
299                if (el) group.push({ el: el, id: ids[i] });
300            }
301            if (group.length) return group;
302        }
303
304        return [{ el: leaderBtn, id: leaderId }];
305    }
306
307    function bindDesktopIconDrag(btn, hooks) {
308        hooks = hooks || {};
309        var dragging = false;
310        var moved = false;
311        var startX = 0;
312        var startY = 0;
313        var origLeft = 0;
314        var origTop = 0;
315        var shortcutId = btn.getAttribute('data-shortcut-id');
316        var groupDrag = null;
317
318        function snapshotGroup(group) {
319            var snaps = [];
320            for (var i = 0; i < group.length; i++) {
321                var item = group[i];
322                snaps.push({
323                    el: item.el,
324                    id: item.id,
325                    origLeft: parseInt(item.el.style.left, 10) || 0,
326                    origTop: parseInt(item.el.style.top, 10) || 0
327                });
328            }
329            return snaps;
330        }
331
332        function setGroupDragging(active, copyMode) {
333            if (!groupDrag) return;
334            for (var i = 0; i < groupDrag.length; i++) {
335                var gEl = groupDrag[i].el;
336                gEl.classList.toggle('citrus-desktop-icon--dragging', !!active);
337                gEl.classList.toggle('citrus-desktop-icon--drop-copy', !!copyMode);
338                gEl.style.pointerEvents = active ? 'none' : '';
339            }
340        }
341
342        function endDesktopDrag(ev) {
343            if (btn.hasPointerCapture(ev.pointerId)) btn.releasePointerCapture(ev.pointerId);
344            btn.classList.remove('citrus-desktop-icon--dragging', 'citrus-desktop-icon--drop-copy');
345            btn.style.pointerEvents = '';
346            setGroupDragging(false, false);
347
348            var dropTarget = dragging ? findDropTarget(ev.clientX, ev.clientY) : null;
349            if (dragging && activeDrag && dropTarget && dropTarget.type === 'folder') {
350                var dropGroup = groupDrag && groupDrag.length ? groupDrag : [{ el: btn, id: shortcutId }];
351                for (var gi = 0; gi < dropGroup.length; gi++) {
352                    var gItem = dropGroup[gi];
353                    var payload = parseDragPayload(gItem.el, 'desktop');
354                    handleDrop(payload, dropTarget, ev.shiftKey, null);
355                }
356                if (!ev.shiftKey && global.CitrusDesktopSelection &&
357                    typeof global.CitrusDesktopSelection.clear === 'function') {
358                    global.CitrusDesktopSelection.clear();
359                }
360            } else if (dragging) {
361                var shortcuts = global.CitrusDesktopShortcuts;
362                var desktop = document.getElementById('citrusos-desktop');
363                var repositionGroup = groupDrag && groupDrag.length ? groupDrag : [{
364                    el: btn,
365                    id: shortcutId,
366                    origLeft: origLeft,
367                    origTop: origTop
368                }];
369                var dropMembers = [];
370                for (var ri = 0; ri < repositionGroup.length; ri++) {
371                    var icon = repositionGroup[ri];
372                    dropMembers.push({
373                        id: icon.id,
374                        x: parseInt(icon.el.style.left, 10) || 0,
375                        y: parseInt(icon.el.style.top, 10) || 0
376                    });
377                }
378                var resolved = shortcuts && shortcuts.resolveGroupDropPositions
379                    ? shortcuts.resolveGroupDropPositions(dropMembers, desktop)
380                    : dropMembers;
381                if (shortcuts && shortcuts.updateGroupPositions) {
382                    shortcuts.updateGroupPositions(resolved);
383                } else {
384                    for (var ui = 0; ui < resolved.length; ui++) {
385                        if (shortcuts && shortcuts.updateShortcutPosition) {
386                            shortcuts.updateShortcutPosition(resolved[ui].id, resolved[ui].x, resolved[ui].y);
387                        }
388                    }
389                }
390                for (var pi = 0; pi < resolved.length; pi++) {
391                    var placed = resolved[pi];
392                    var placedEl = desktop && desktop.querySelector(
393                        '.citrus-desktop-icon[data-shortcut-id="' + placed.id + '"]'
394                    );
395                    if (placedEl) {
396                        placedEl.style.left = placed.x + 'px';
397                        placedEl.style.top = placed.y + 'px';
398                    }
399                }
400                if (hooks.onReposition) hooks.onReposition();
401            }
402
403            clearDropHighlights();
404            activeDrag = null;
405            dragging = false;
406            groupDrag = null;
407            if (moved) {
408                btn.setAttribute('data-drag-moved', '1');
409                if (hooks.onDragEnd) hooks.onDragEnd(moved);
410            }
411            moved = false;
412        }
413
414        btn.addEventListener('pointerdown', function (ev) {
415            if (ev.button !== 0) return;
416            if (activeDrag) return;
417            btn.removeAttribute('data-drag-moved');
418            dragging = false;
419            moved = false;
420            groupDrag = null;
421            startX = ev.clientX;
422            startY = ev.clientY;
423            origLeft = parseInt(btn.style.left, 10) || 0;
424            origTop = parseInt(btn.style.top, 10) || 0;
425            btn.setPointerCapture(ev.pointerId);
426        });
427
428        btn.addEventListener('pointermove', function (ev) {
429            if (!btn.hasPointerCapture(ev.pointerId)) return;
430            var dx = ev.clientX - startX;
431            var dy = ev.clientY - startY;
432            if (!moved && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) {
433                moved = true;
434                dragging = true;
435                groupDrag = snapshotGroup(collectDesktopGroupDrag(btn));
436                btn.classList.add('citrus-desktop-icon--dragging');
437                btn.style.pointerEvents = 'none';
438                setGroupDragging(true, !!ev.shiftKey);
439                activeDrag = {
440                    sourceType: 'desktop',
441                    sourceEl: btn,
442                    payload: parseDragPayload(btn, 'desktop'),
443                    group: groupDrag
444                };
445            }
446            if (!dragging) return;
447
448            btn.classList.toggle('citrus-desktop-icon--drop-copy', !!ev.shiftKey);
449            setGroupDragging(true, !!ev.shiftKey);
450            findDropTarget(ev.clientX, ev.clientY);
451
452            var logical = pointerDeltaToLogical(dx, dy);
453            var shortcuts = global.CitrusDesktopShortcuts;
454            var desktop = document.getElementById('citrusos-desktop');
455            var clamped = shortcuts && shortcuts.clampGroupDelta
456                ? shortcuts.clampGroupDelta(groupDrag, logical.dx, logical.dy, desktop)
457                : { dx: logical.dx, dy: logical.dy };
458            for (var mi = 0; mi < groupDrag.length; mi++) {
459                var member = groupDrag[mi];
460                member.el.style.left = (member.origLeft + clamped.dx) + 'px';
461                member.el.style.top = (member.origTop + clamped.dy) + 'px';
462            }
463        });
464
465        btn.addEventListener('pointerup', endDesktopDrag);
466        btn.addEventListener('pointercancel', endDesktopDrag);
467    }
468
469    function bindFolderIconDrag(btn, context) {
470        context = context || {};
471        var dragging = false;
472        var moved = false;
473        var startX = 0;
474        var startY = 0;
475
476        btn.addEventListener('pointerdown', function (ev) {
477            if (ev.button !== 0) return;
478            dragging = false;
479            moved = false;
480            startX = ev.clientX;
481            startY = ev.clientY;
482            btn.setPointerCapture(ev.pointerId);
483        });
484
485        btn.addEventListener('pointermove', function (ev) {
486            if (!btn.hasPointerCapture(ev.pointerId)) return;
487            var dx = ev.clientX - startX;
488            var dy = ev.clientY - startY;
489            if (!moved && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) {
490                moved = true;
491                dragging = true;
492                activeDrag = {
493                    sourceType: 'folder',
494                    sourceEl: btn,
495                    sourceContext: context,
496                    payload: parseDragPayload(btn, 'folder'),
497                    ghost: createGhost(btn, findIconLabel(btn)),
498                    copyMode: false
499                };
500                if (activeDrag.ghost) moveGhost(activeDrag.ghost, ev.clientX, ev.clientY, false);
501            }
502            if (!dragging || !activeDrag) return;
503            findDropTarget(ev.clientX, ev.clientY);
504            if (activeDrag.ghost) moveGhost(activeDrag.ghost, ev.clientX, ev.clientY, !!ev.shiftKey);
505        });
506
507        function endFolderDrag(ev) {
508            if (btn.hasPointerCapture(ev.pointerId)) btn.releasePointerCapture(ev.pointerId);
509            if (dragging && activeDrag) {
510                var dropTarget = findDropTarget(ev.clientX, ev.clientY);
511                if (dropTarget) {
512                    handleDrop(activeDrag.payload, dropTarget, !!ev.shiftKey, context, {
513                        clientX: ev.clientX,
514                        clientY: ev.clientY
515                    });
516                }
517            }
518            destroyGhost();
519            clearDropHighlights();
520            activeDrag = null;
521            dragging = false;
522            moved = false;
523        }
524
525        btn.addEventListener('pointerup', endFolderDrag);
526        btn.addEventListener('pointercancel', endFolderDrag);
527    }
528
529    function bindFolderGridDropZone(grid, context) {
530        /* drop targeting handled via elementFromPoint in pointer handlers */
531    }
532
533    global.CitrusDesktopDnD = {
534        bindDesktopIconDrag: bindDesktopIconDrag,
535        bindFolderIconDrag: bindFolderIconDrag,
536        bindFolderGridDropZone: bindFolderGridDropZone,
537        parseDragPayload: parseDragPayload,
538        handleDrop: handleDrop,
539        isDragActive: function () { return !!activeDrag; }
540    };
541})(typeof window !== 'undefined' ? window : this);

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.