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.