') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Add support for RMB/Escape canceling layer drag reordering in the Layers panel by Keavon · Pull Request #3426 · GraphiteEditor/Graphite · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions frontend/src/components/Editor.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,6 @@

import { type Editor } from "@graphite/editor";
import { createClipboardManager } from "@graphite/io-managers/clipboard";
import { createDragManager } from "@graphite/io-managers/drag";
import { createHyperlinkManager } from "@graphite/io-managers/hyperlinks";
import { createInputManager } from "@graphite/io-managers/input";
import { createLocalizationManager } from "@graphite/io-managers/localization";
Expand DownExpand Up@@ -46,7 +45,6 @@
createLocalizationManager(editor);
createPanicManager(editor, dialog);
createPersistenceManager(editor, portfolio);
let dragManagerDestructor = createDragManager();
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen);

onMount(() => {
Expand All@@ -56,7 +54,6 @@

onDestroy(() => {
// Call the destructor for each manager
dragManagerDestructor();
inputManagerDestructor();
});
</script>
Expand Down
219 changes: 152 additions & 67 deletions frontend/src/components/panels/Layers.svelte
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@
import { getContext, onMount, onDestroy, tick } from "svelte";

import type { Editor } from "@graphite/editor";
import { beginDraggingElement } from "@graphite/io-managers/drag";
import {
defaultWidgetLayout,
patchWidgetLayout,
Expand DownExpand Up@@ -40,6 +39,14 @@
markerHeight: number;
};

type InternalDragState = {
active: boolean;
layerId: bigint;
listing: LayerListingInfo;
startX: number;
startY: number;
};

const editor = getContext<Editor>("editor");
const nodeGraph = getContext<NodeGraphState>("nodeGraph");

Expand All@@ -52,7 +59,9 @@
// Interactive dragging
let draggable = true;
let draggingData: undefined | DraggingData = undefined;
let internalDragState: InternalDragState | undefined = undefined;
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined;
let justFinishedDrag = false; // Used to prevent click events after a drag
let dragInPanel = false;

// Interactive clipping
Expand DownExpand Up@@ -92,6 +101,11 @@
updateLayerInTree(targetId, targetLayer);
});

addEventListener("pointerup", draggingPointerUp);
addEventListener("pointermove", draggingPointerMove);
addEventListener("mousedown", draggingMouseDown);
addEventListener("keydown", draggingKeyDown);

addEventListener("pointermove", clippingHover);
addEventListener("keydown", clippingKeyPress);
addEventListener("keyup", clippingKeyPress);
Expand All@@ -104,6 +118,11 @@
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructureJs);
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerDetails);

removeEventListener("pointerup", draggingPointerUp);
removeEventListener("pointermove", draggingPointerMove);
removeEventListener("mousedown", draggingMouseDown);
removeEventListener("keydown", draggingKeyDown);

removeEventListener("pointermove", clippingHover);
removeEventListener("keydown", clippingKeyPress);
removeEventListener("keyup", clippingKeyPress);
Expand DownExpand Up@@ -223,6 +242,13 @@
}

function selectLayerWithModifiers(e: MouseEvent, listing: LayerListingInfo) {
if (justFinishedDrag) {
justFinishedDrag = false;
// Prevent bubbling to deselectAllLayers
e.stopPropagation();
return;
}

// Get the pressed state of the modifier keys
const [ctrl, meta, shift, alt] = [e.ctrlKey, e.metaKey, e.shiftKey, e.altKey];
// Get the state of the platform's accel key and its opposite platform's accel key
Expand DownExpand Up@@ -255,7 +281,7 @@
return;
}

// Check if the cursor is near the border btween two layers
// Check if the cursor is near the border between two layers
const DISTANCE = 6;
const distanceFromTop = e.clientY - target.getBoundingClientRect().top;
const distanceFromBottom = target.getBoundingClientRect().bottom - e.clientY;
Expand DownExpand Up@@ -288,6 +314,11 @@
}

async function deselectAllLayers() {
if (justFinishedDrag) {
justFinishedDrag = false;
return;
}

editor.handle.deselectAllLayers();
}

Expand DownExpand Up@@ -371,83 +402,135 @@
};
}

async function dragStart(event: DragEvent, listing: LayerListingInfo) {
const layer = listing.entry;
dragInPanel = true;
if (!$nodeGraph.selected.includes(layer.id)) {
fakeHighlightOfNotYetSelectedLayerBeingDragged = layer.id;
}
const select = () => {
if (!$nodeGraph.selected.includes(layer.id)) selectLayer(listing, false, false);
function layerPointerDown(e: PointerEvent, listing: LayerListingInfo) {
// Only left click drags
if (e.button !== 0 || !draggable) return;

internalDragState = {
active: false,
layerId: listing.entry.id,
listing: listing,
startX: e.clientX,
startY: e.clientY,
};
}

function draggingPointerMove(e: PointerEvent) {
if (!internalDragState || !list) return;

// Calculate distance moved
if (!internalDragState.active) {
const distance = Math.hypot(e.clientX - internalDragState.startX, e.clientY - internalDragState.startY);
const DRAG_THRESHOLD = 5;

if (distance > DRAG_THRESHOLD) {
internalDragState.active = true;
dragInPanel = true;

const layer = internalDragState.listing.entry;
if (!$nodeGraph.selected.includes(layer.id)) {
fakeHighlightOfNotYetSelectedLayerBeingDragged = layer.id;
}
}
}

// Perform drag calculations if a drag is occurring
if (internalDragState.active) {
const select = () => {
if (internalDragState && !$nodeGraph.selected.includes(internalDragState.layerId)) {
selectLayer(internalDragState.listing, false, false);
}
};

draggingData = calculateDragIndex(list, e.clientY, select);
}
}

function draggingPointerUp() {
if (internalDragState?.active && draggingData) {
const { select, insertParentId, insertIndex } = draggingData;

// Commit the move
select?.();
editor.handle.moveLayerInTree(insertParentId, insertIndex);

// Prevent the subsequent click event from processing
justFinishedDrag = true;
} else if (justFinishedDrag) {
// Avoid right-click abort getting stuck with `justFinishedDrag` set and blocking the first subsequent click to select a layer
setTimeout(() => {
justFinishedDrag = false;
}, 0);
}

const target = (event.target instanceof HTMLElement && event.target) || undefined;
const closest = target?.closest("[data-layer]") || undefined;
const draggingELement = (closest instanceof HTMLElement && closest) || undefined;
if (draggingELement) beginDraggingElement(draggingELement);
// Reset state
abortDrag();
}

// Set style of cursor for drag
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
event.dataTransfer.effectAllowed = "move";
function abortDrag() {
internalDragState = undefined;
draggingData = undefined;
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
dragInPanel = false;
}

function draggingMouseDown(e: MouseEvent) {
// Abort if a drag is active and the user presses the right mouse button (button 2)
if (e.button === 2 && internalDragState?.active) {
justFinishedDrag = true;
abortDrag();
}
}

if (list) draggingData = calculateDragIndex(list, event.clientY, select);
function draggingKeyDown(e: KeyboardEvent) {
if (e.key === "Escape" && internalDragState?.active) {
justFinishedDrag = true;
abortDrag();
}
}

function updateInsertLine(event: DragEvent) {
if (!draggable) return;
function fileDragOver(e: DragEvent) {
if (!draggable || !e.dataTransfer || !e.dataTransfer.types.includes("Files")) return;

// Stop the drag from being shown as cancelled
event.preventDefault();
e.preventDefault();
dragInPanel = true;

if (list) draggingData = calculateDragIndex(list, event.clientY, draggingData?.select);
if (list) draggingData = calculateDragIndex(list, e.clientY);
}

function drop(e: DragEvent) {
if (!draggingData) return;
const { select, insertParentId, insertIndex } = draggingData;
function fileDrop(e: DragEvent) {
if (!draggingData || !e.dataTransfer || !e.dataTransfer.types.includes("Files")) return;

const { insertParentId, insertIndex } = draggingData;

e.preventDefault();

if (e.dataTransfer) {
// Moving layers
if (e.dataTransfer.items.length === 0) {
if (draggable && dragInPanel) {
select?.();
editor.handle.moveLayerInTree(insertParentId, insertIndex);
}
Array.from(e.dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile();
if (!file) return;

if (file.type.includes("svg")) {
const svgData = await file.text();
editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex);
return;
}
// Importing files
else {
Array.from(e.dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile();
if (!file) return;

if (file.type.includes("svg")) {
const svgData = await file.text();
editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex);
return;
}

if (file.type.startsWith("image")) {
const imageData = await extractPixelData(file);
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex);
return;
}
if (file.type.startsWith("image")) {
const imageData = await extractPixelData(file);
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex);
return;
}

// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
const graphiteFileSuffix = "." + editor.handle.fileExtension();
if (file.name.endsWith(graphiteFileSuffix)) {
const content = await file.text();
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
editor.handle.openDocumentFile(documentName, content);
return;
}
});
// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
const graphiteFileSuffix = "." + editor.handle.fileExtension();
if (file.name.endsWith(graphiteFileSuffix)) {
const content = await file.text();
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
editor.handle.openDocumentFile(documentName, content);
return;
}
}
});

draggingData = undefined;
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
Expand DownExpand Up@@ -502,16 +585,15 @@
{/if}
<WidgetLayout layout={layersPanelControlBarRightLayout} />
</LayoutRow>
<LayoutRow class="list-area" scrollableY={true}>
<LayoutRow class="list-area" classes={{ "drag-ongoing": Boolean(internalDragState?.active && draggingData) }} scrollableY={true}>
<LayoutCol
class="list"
styles={{ cursor: layerToClipUponClick && layerToClipAltKeyPressed && layerToClipUponClick.entry.clippable ? "alias" : "auto" }}
data-layer-panel
bind:this={list}
on:click={() => deselectAllLayers()}
on:dragover={updateInsertLine}
on:dragend={drop}
on:drop={drop}
on:dragover={fileDragOver}
on:drop={fileDrop}
>
{#each layers as listing, index}
{@const selected = fakeHighlightOfNotYetSelectedLayerBeingDragged !== undefined ? fakeHighlightOfNotYetSelectedLayerBeingDragged === listing.entry.id : listing.entry.selected}
Expand All@@ -528,8 +610,7 @@
data-layer
data-index={index}
tooltip={listing.entry.tooltip}
{draggable}
on:dragstart={(e) => draggable && dragStart(e, listing)}
on:pointerdown={(e) => layerPointerDown(e, listing)}
on:click={(e) => selectLayerWithModifiers(e, listing)}
>
{#if listing.entry.childrenAllowed}
Expand DownExpand Up@@ -642,10 +723,14 @@
// Layer hierarchy
.list-area {
position: relative;
margin-top: 4px;
padding-top: 4px;
// Combine with the bottom bar to avoid a double border
margin-bottom: -1px;

&.drag-ongoing .layer {
pointer-events: none;
}

.layer {
flex: 0 0 auto;
align-items: center;
Expand DownExpand Up@@ -813,7 +898,7 @@
left: 4px;
right: 4px;
background: var(--color-e-nearwhite);
margin-top: -3px;
margin-top: 1px;
height: 5px;
z-index: 1;
pointer-events: none;
Expand Down
24 changes: 0 additions & 24 deletions frontend/src/io-managers/drag.ts

This file was deleted.