|
2 | 2 | import { getContext, onMount, onDestroy, tick } from "svelte"; |
3 | 3 |
|
4 | 4 | import type { Editor } from "@graphite/editor"; |
5 | | - import { beginDraggingElement } from "@graphite/io-managers/drag"; |
6 | 5 | import { |
7 | 6 | defaultWidgetLayout, |
8 | 7 | patchWidgetLayout, |
|
40 | 39 | markerHeight: number; |
41 | 40 | }; |
42 | 41 |
|
| 42 | + type InternalDragState = { |
| 43 | + active: boolean; |
| 44 | + layerId: bigint; |
| 45 | + listing: LayerListingInfo; |
| 46 | + startX: number; |
| 47 | + startY: number; |
| 48 | + }; |
| 49 | +
|
43 | 50 | const editor = getContext<Editor>("editor"); |
44 | 51 | const nodeGraph = getContext<NodeGraphState>("nodeGraph"); |
45 | 52 |
|
|
52 | 59 | // Interactive dragging |
53 | 60 | let draggable = true; |
54 | 61 | let draggingData: undefined | DraggingData = undefined; |
| 62 | + let internalDragState: InternalDragState | undefined = undefined; |
55 | 63 | let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined; |
| 64 | + let justFinishedDrag = false; // Used to prevent click events after a drag |
56 | 65 | let dragInPanel = false; |
57 | 66 |
|
58 | 67 | // Interactive clipping |
|
92 | 101 | updateLayerInTree(targetId, targetLayer); |
93 | 102 | }); |
94 | 103 |
|
| 104 | + addEventListener("pointerup", draggingPointerUp); |
| 105 | + addEventListener("pointermove", draggingPointerMove); |
| 106 | + addEventListener("mousedown", draggingMouseDown); |
| 107 | + addEventListener("keydown", draggingKeyDown); |
| 108 | +
|
95 | 109 | addEventListener("pointermove", clippingHover); |
96 | 110 | addEventListener("keydown", clippingKeyPress); |
97 | 111 | addEventListener("keyup", clippingKeyPress); |
|
104 | 118 | editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructureJs); |
105 | 119 | editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerDetails); |
106 | 120 |
|
| 121 | + removeEventListener("pointerup", draggingPointerUp); |
| 122 | + removeEventListener("pointermove", draggingPointerMove); |
| 123 | + removeEventListener("mousedown", draggingMouseDown); |
| 124 | + removeEventListener("keydown", draggingKeyDown); |
| 125 | +
|
107 | 126 | removeEventListener("pointermove", clippingHover); |
108 | 127 | removeEventListener("keydown", clippingKeyPress); |
109 | 128 | removeEventListener("keyup", clippingKeyPress); |
|
223 | 242 | } |
224 | 243 |
|
225 | 244 | function selectLayerWithModifiers(e: MouseEvent, listing: LayerListingInfo) { |
| 245 | + if (justFinishedDrag) { |
| 246 | + justFinishedDrag = false; |
| 247 | + // Prevent bubbling to deselectAllLayers |
| 248 | + e.stopPropagation(); |
| 249 | + return; |
| 250 | + } |
| 251 | +
|
226 | 252 | // Get the pressed state of the modifier keys |
227 | 253 | const [ctrl, meta, shift, alt] = [e.ctrlKey, e.metaKey, e.shiftKey, e.altKey]; |
228 | 254 | // Get the state of the platform's accel key and its opposite platform's accel key |
|
255 | 281 | return; |
256 | 282 | } |
257 | 283 |
|
258 | | - // Check if the cursor is near the border btween two layers |
| 284 | + // Check if the cursor is near the border between two layers |
259 | 285 | const DISTANCE = 6; |
260 | 286 | const distanceFromTop = e.clientY - target.getBoundingClientRect().top; |
261 | 287 | const distanceFromBottom = target.getBoundingClientRect().bottom - e.clientY; |
|
288 | 314 | } |
289 | 315 |
|
290 | 316 | async function deselectAllLayers() { |
| 317 | + if (justFinishedDrag) { |
| 318 | + justFinishedDrag = false; |
| 319 | + return; |
| 320 | + } |
| 321 | +
|
291 | 322 | editor.handle.deselectAllLayers(); |
292 | 323 | } |
293 | 324 |
|
|
371 | 402 | }; |
372 | 403 | } |
373 | 404 |
|
374 | | - async function dragStart(event: DragEvent, listing: LayerListingInfo) { |
375 | | - const layer = listing.entry; |
376 | | - dragInPanel = true; |
377 | | - if (!$nodeGraph.selected.includes(layer.id)) { |
378 | | - fakeHighlightOfNotYetSelectedLayerBeingDragged = layer.id; |
379 | | - } |
380 | | - const select = () => { |
381 | | - if (!$nodeGraph.selected.includes(layer.id)) selectLayer(listing, false, false); |
| 405 | + function layerPointerDown(e: PointerEvent, listing: LayerListingInfo) { |
| 406 | + // Only left click drags |
| 407 | + if (e.button !== 0 || !draggable) return; |
| 408 | +
|
| 409 | + internalDragState = { |
| 410 | + active: false, |
| 411 | + layerId: listing.entry.id, |
| 412 | + listing: listing, |
| 413 | + startX: e.clientX, |
| 414 | + startY: e.clientY, |
382 | 415 | }; |
| 416 | + } |
| 417 | +
|
| 418 | + function draggingPointerMove(e: PointerEvent) { |
| 419 | + if (!internalDragState || !list) return; |
| 420 | +
|
| 421 | + // Calculate distance moved |
| 422 | + if (!internalDragState.active) { |
| 423 | + const distance = Math.hypot(e.clientX - internalDragState.startX, e.clientY - internalDragState.startY); |
| 424 | + const DRAG_THRESHOLD = 5; |
| 425 | +
|
| 426 | + if (distance > DRAG_THRESHOLD) { |
| 427 | + internalDragState.active = true; |
| 428 | + dragInPanel = true; |
| 429 | +
|
| 430 | + const layer = internalDragState.listing.entry; |
| 431 | + if (!$nodeGraph.selected.includes(layer.id)) { |
| 432 | + fakeHighlightOfNotYetSelectedLayerBeingDragged = layer.id; |
| 433 | + } |
| 434 | + } |
| 435 | + } |
| 436 | +
|
| 437 | + // Perform drag calculations if a drag is occurring |
| 438 | + if (internalDragState.active) { |
| 439 | + const select = () => { |
| 440 | + if (internalDragState && !$nodeGraph.selected.includes(internalDragState.layerId)) { |
| 441 | + selectLayer(internalDragState.listing, false, false); |
| 442 | + } |
| 443 | + }; |
| 444 | +
|
| 445 | + draggingData = calculateDragIndex(list, e.clientY, select); |
| 446 | + } |
| 447 | + } |
| 448 | +
|
| 449 | + function draggingPointerUp() { |
| 450 | + if (internalDragState?.active && draggingData) { |
| 451 | + const { select, insertParentId, insertIndex } = draggingData; |
| 452 | +
|
| 453 | + // Commit the move |
| 454 | + select?.(); |
| 455 | + editor.handle.moveLayerInTree(insertParentId, insertIndex); |
| 456 | +
|
| 457 | + // Prevent the subsequent click event from processing |
| 458 | + justFinishedDrag = true; |
| 459 | + } else if (justFinishedDrag) { |
| 460 | + // Avoid right-click abort getting stuck with `justFinishedDrag` set and blocking the first subsequent click to select a layer |
| 461 | + setTimeout(() => { |
| 462 | + justFinishedDrag = false; |
| 463 | + }, 0); |
| 464 | + } |
383 | 465 |
|
384 | | - const target = (event.target instanceof HTMLElement && event.target) || undefined; |
385 | | - const closest = target?.closest("[data-layer]") || undefined; |
386 | | - const draggingELement = (closest instanceof HTMLElement && closest) || undefined; |
387 | | - if (draggingELement) beginDraggingElement(draggingELement); |
| 466 | + // Reset state |
| 467 | + abortDrag(); |
| 468 | + } |
388 | 469 |
|
389 | | - // Set style of cursor for drag |
390 | | - if (event.dataTransfer) { |
391 | | - event.dataTransfer.dropEffect = "move"; |
392 | | - event.dataTransfer.effectAllowed = "move"; |
| 470 | + function abortDrag() { |
| 471 | + internalDragState = undefined; |
| 472 | + draggingData = undefined; |
| 473 | + fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined; |
| 474 | + dragInPanel = false; |
| 475 | + } |
| 476 | +
|
| 477 | + function draggingMouseDown(e: MouseEvent) { |
| 478 | + // Abort if a drag is active and the user presses the right mouse button (button 2) |
| 479 | + if (e.button === 2 && internalDragState?.active) { |
| 480 | + justFinishedDrag = true; |
| 481 | + abortDrag(); |
393 | 482 | } |
| 483 | + } |
394 | 484 |
|
395 | | - if (list) draggingData = calculateDragIndex(list, event.clientY, select); |
| 485 | + function draggingKeyDown(e: KeyboardEvent) { |
| 486 | + if (e.key === "Escape" && internalDragState?.active) { |
| 487 | + justFinishedDrag = true; |
| 488 | + abortDrag(); |
| 489 | + } |
396 | 490 | } |
397 | 491 |
|
398 | | - function updateInsertLine(event: DragEvent) { |
399 | | - if (!draggable) return; |
| 492 | + function fileDragOver(e: DragEvent) { |
| 493 | + if (!draggable || !e.dataTransfer || !e.dataTransfer.types.includes("Files")) return; |
400 | 494 |
|
401 | 495 | // Stop the drag from being shown as cancelled |
402 | | - event.preventDefault(); |
| 496 | + e.preventDefault(); |
403 | 497 | dragInPanel = true; |
404 | 498 |
|
405 | | - if (list) draggingData = calculateDragIndex(list, event.clientY, draggingData?.select); |
| 499 | + if (list) draggingData = calculateDragIndex(list, e.clientY); |
406 | 500 | } |
407 | 501 |
|
408 | | - function drop(e: DragEvent) { |
409 | | - if (!draggingData) return; |
410 | | - const { select, insertParentId, insertIndex } = draggingData; |
| 502 | + function fileDrop(e: DragEvent) { |
| 503 | + if (!draggingData || !e.dataTransfer || !e.dataTransfer.types.includes("Files")) return; |
| 504 | +
|
| 505 | + const { insertParentId, insertIndex } = draggingData; |
411 | 506 |
|
412 | 507 | e.preventDefault(); |
413 | 508 |
|
414 | | - if (e.dataTransfer) { |
415 | | - // Moving layers |
416 | | - if (e.dataTransfer.items.length === 0) { |
417 | | - if (draggable && dragInPanel) { |
418 | | - select?.(); |
419 | | - editor.handle.moveLayerInTree(insertParentId, insertIndex); |
420 | | - } |
| 509 | + Array.from(e.dataTransfer.items).forEach(async (item) => { |
| 510 | + const file = item.getAsFile(); |
| 511 | + if (!file) return; |
| 512 | +
|
| 513 | + if (file.type.includes("svg")) { |
| 514 | + const svgData = await file.text(); |
| 515 | + editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex); |
| 516 | + return; |
421 | 517 | } |
422 | | - // Importing files |
423 | | - else { |
424 | | - Array.from(e.dataTransfer.items).forEach(async (item) => { |
425 | | - const file = item.getAsFile(); |
426 | | - if (!file) return; |
427 | | -
|
428 | | - if (file.type.includes("svg")) { |
429 | | - const svgData = await file.text(); |
430 | | - editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex); |
431 | | - return; |
432 | | - } |
433 | 518 |
|
434 | | - if (file.type.startsWith("image")) { |
435 | | - const imageData = await extractPixelData(file); |
436 | | - editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex); |
437 | | - return; |
438 | | - } |
| 519 | + if (file.type.startsWith("image")) { |
| 520 | + const imageData = await extractPixelData(file); |
| 521 | + editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex); |
| 522 | + return; |
| 523 | + } |
439 | 524 |
|
440 | | - // When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab |
441 | | - const graphiteFileSuffix = "." + editor.handle.fileExtension(); |
442 | | - if (file.name.endsWith(graphiteFileSuffix)) { |
443 | | - const content = await file.text(); |
444 | | - const documentName = file.name.slice(0, -graphiteFileSuffix.length); |
445 | | - editor.handle.openDocumentFile(documentName, content); |
446 | | - return; |
447 | | - } |
448 | | - }); |
| 525 | + // When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab |
| 526 | + const graphiteFileSuffix = "." + editor.handle.fileExtension(); |
| 527 | + if (file.name.endsWith(graphiteFileSuffix)) { |
| 528 | + const content = await file.text(); |
| 529 | + const documentName = file.name.slice(0, -graphiteFileSuffix.length); |
| 530 | + editor.handle.openDocumentFile(documentName, content); |
| 531 | + return; |
449 | 532 | } |
450 | | - } |
| 533 | + }); |
451 | 534 |
|
452 | 535 | draggingData = undefined; |
453 | 536 | fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined; |
|
502 | 585 | {/if} |
503 | 586 | <WidgetLayout layout={layersPanelControlBarRightLayout} /> |
504 | 587 | </LayoutRow> |
505 | | - <LayoutRow class="list-area" scrollableY={true}> |
| 588 | + <LayoutRow class="list-area" classes={{ "drag-ongoing": Boolean(internalDragState?.active && draggingData) }} scrollableY={true}> |
506 | 589 | <LayoutCol |
507 | 590 | class="list" |
508 | 591 | styles={{ cursor: layerToClipUponClick && layerToClipAltKeyPressed && layerToClipUponClick.entry.clippable ? "alias" : "auto" }} |
509 | 592 | data-layer-panel |
510 | 593 | bind:this={list} |
511 | 594 | on:click={() => deselectAllLayers()} |
512 | | - on:dragover={updateInsertLine} |
513 | | - on:dragend={drop} |
514 | | - on:drop={drop} |
| 595 | + on:dragover={fileDragOver} |
| 596 | + on:drop={fileDrop} |
515 | 597 | > |
516 | 598 | {#each layers as listing, index} |
517 | 599 | {@const selected = fakeHighlightOfNotYetSelectedLayerBeingDragged !== undefined ? fakeHighlightOfNotYetSelectedLayerBeingDragged === listing.entry.id : listing.entry.selected} |
|
528 | 610 | data-layer |
529 | 611 | data-index={index} |
530 | 612 | tooltip={listing.entry.tooltip} |
531 | | - {draggable} |
532 | | - on:dragstart={(e) => draggable && dragStart(e, listing)} |
| 613 | + on:pointerdown={(e) => layerPointerDown(e, listing)} |
533 | 614 | on:click={(e) => selectLayerWithModifiers(e, listing)} |
534 | 615 | > |
535 | 616 | {#if listing.entry.childrenAllowed} |
|
642 | 723 | // Layer hierarchy |
643 | 724 | .list-area { |
644 | 725 | position: relative; |
645 | | - margin-top: 4px; |
| 726 | + padding-top: 4px; |
646 | 727 | // Combine with the bottom bar to avoid a double border |
647 | 728 | margin-bottom: -1px; |
648 | 729 |
|
| 730 | + &.drag-ongoing .layer { |
| 731 | + pointer-events: none; |
| 732 | + } |
| 733 | +
|
649 | 734 | .layer { |
650 | 735 | flex: 0 0 auto; |
651 | 736 | align-items: center; |
|
813 | 898 | left: 4px; |
814 | 899 | right: 4px; |
815 | 900 | background: var(--color-e-nearwhite); |
816 | | - margin-top: -3px; |
| 901 | + margin-top: 1px; |
817 | 902 | height: 5px; |
818 | 903 | z-index: 1; |
819 | 904 | pointer-events: none; |
|
0 commit comments