diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index a4293b5b..9aef8ba6 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -925,7 +925,7 @@ impl Fsm for SelectToolFsmState { HintInfo::keys([Key::Control], "Opp. Corner").prepend_plus(), ]), HintGroup(vec![ - HintInfo::keys([Key::Alt], "Move Duplicate"), + HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "Move Duplicate"), HintInfo::keys([Key::Control, Key::KeyD], "Duplicate").add_mac_keys([Key::Command, Key::KeyD]), ]), ]); diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index 6c497982..858da57c 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -539,6 +539,17 @@ impl HintInfo { } } + pub fn keys_and_mouse(keys: impl IntoIterator, mouse_motion: MouseMotion, label: impl Into) -> Self { + let keys: Vec<_> = keys.into_iter().collect(); + Self { + key_groups: vec![KeysGroup(keys).into()], + key_groups_mac: None, + mouse: Some(mouse_motion), + label: label.into(), + plus: false, + } + } + pub fn arrow_keys(label: impl Into) -> Self { HintInfo { key_groups: vec![ diff --git a/frontend/assets/icon-24px-two-tone/vector-freehand-tool.svg b/frontend/assets/icon-24px-two-tone/vector-freehand-tool.svg index fa190bad..2d05b1f7 100644 --- a/frontend/assets/icon-24px-two-tone/vector-freehand-tool.svg +++ b/frontend/assets/icon-24px-two-tone/vector-freehand-tool.svg @@ -1,5 +1,5 @@ - + diff --git a/frontend/src/components/floating-menus/DialogModal.svelte b/frontend/src/components/floating-menus/DialogModal.svelte index 82f237fc..a0e2790c 100644 --- a/frontend/src/components/floating-menus/DialogModal.svelte +++ b/frontend/src/components/floating-menus/DialogModal.svelte @@ -11,7 +11,7 @@ const dialog = getContext("dialog"); - let self: FloatingMenu; + let self: FloatingMenu | undefined; export function dismiss() { dialog.dismissDialog(); @@ -19,7 +19,7 @@ onMount(() => { // Focus the first button in the popup - const emphasizedOrFirstButton = (self.div().querySelector("[data-emphasized]") || self.div().querySelector("[data-text-button]") || undefined) as HTMLButtonElement | undefined; + const emphasizedOrFirstButton = (self?.div()?.querySelector("[data-emphasized]") || self?.div()?.querySelector("[data-text-button]") || undefined) as HTMLButtonElement | undefined; emphasizedOrFirstButton?.focus(); }); diff --git a/frontend/src/components/floating-menus/EyedropperPreview.svelte b/frontend/src/components/floating-menus/EyedropperPreview.svelte index dcff1efa..9c666526 100644 --- a/frontend/src/components/floating-menus/EyedropperPreview.svelte +++ b/frontend/src/components/floating-menus/EyedropperPreview.svelte @@ -1,5 +1,5 @@
- +
diff --git a/frontend/src/components/floating-menus/MenuList.svelte b/frontend/src/components/floating-menus/MenuList.svelte index 7aada17b..864d3d70 100644 --- a/frontend/src/components/floating-menus/MenuList.svelte +++ b/frontend/src/components/floating-menus/MenuList.svelte @@ -13,8 +13,8 @@ import TextLabel from "@/components/widgets/labels/TextLabel.svelte"; import UserInputLabel from "@/components/widgets/labels/UserInputLabel.svelte"; - let self: FloatingMenu; - let scroller: LayoutCol; + let self: FloatingMenu | undefined; + let scroller: LayoutCol | undefined; // emits: ["update:open", "update:activeEntry", "naturalWidth"], const dispatch = createEventDispatcher<{ open: boolean; activeEntry: MenuListEntry }>(); @@ -171,7 +171,7 @@ } export function scrollViewTo(distanceDown: number): void { - scroller.div().scrollTo(0, distanceDown); + scroller?.div()?.scrollTo(0, distanceDown); } @@ -236,6 +236,7 @@ {/if} {#if entry.children} + {/if} diff --git a/frontend/src/components/layout/FloatingMenu.svelte b/frontend/src/components/layout/FloatingMenu.svelte index 56bcb356..0514c2ed 100644 --- a/frontend/src/components/layout/FloatingMenu.svelte +++ b/frontend/src/components/layout/FloatingMenu.svelte @@ -28,10 +28,10 @@ export let escapeCloses = true; export let strayCloses = true; - let tail: HTMLDivElement; - let self: HTMLDivElement; - let floatingMenuContainer: HTMLDivElement; - let floatingMenuContent: LayoutCol; + let tail: HTMLDivElement | undefined; + let self: HTMLDivElement | undefined; + let floatingMenuContainer: HTMLDivElement | undefined; + let floatingMenuContent: LayoutCol | undefined; // The resize observer is attached to the floating menu container, which is the zero-height div of the width of the parent element's floating menu spawner. // Since CSS doesn't let us make the floating menu (with `position: fixed`) have a 100% width of this container, we need to use JS to observe its size and @@ -82,8 +82,10 @@ await tick(); // Start a new observation of the now-open floating menu - containerResizeObserver.disconnect(); - containerResizeObserver.observe(floatingMenuContainer); + if (floatingMenuContainer) { + containerResizeObserver.disconnect(); + containerResizeObserver.observe(floatingMenuContainer); + } } // Switching from open to closed @@ -117,12 +119,13 @@ const workspace = document.querySelector("[data-workspace]"); - if (!workspace || !self || !floatingMenuContainer || !floatingMenuContent) return; + const floatingMenuContentDiv = floatingMenuContent?.div(); + if (!workspace || !self || !floatingMenuContainer || !floatingMenuContent || !floatingMenuContentDiv) return; workspaceBounds = workspace.getBoundingClientRect(); floatingMenuBounds = self.getBoundingClientRect(); const floatingMenuContainerBounds = floatingMenuContainer.getBoundingClientRect(); - floatingMenuContentBounds = floatingMenuContent.div().getBoundingClientRect(); + floatingMenuContentBounds = floatingMenuContentDiv.getBoundingClientRect(); const inParentFloatingMenu = Boolean(floatingMenuContainer.closest("[data-floating-menu-content]")); @@ -130,10 +133,10 @@ // Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping) // We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever const tailOffset = type === "Popover" ? 10 : 0; - if (direction === "Bottom") floatingMenuContent.div().style.top = `${tailOffset + floatingMenuBounds.top}px`; - if (direction === "Top") floatingMenuContent.div().style.bottom = `${tailOffset + floatingMenuBounds.bottom}px`; - if (direction === "Right") floatingMenuContent.div().style.left = `${tailOffset + floatingMenuBounds.left}px`; - if (direction === "Left") floatingMenuContent.div().style.right = `${tailOffset + floatingMenuBounds.right}px`; + if (direction === "Bottom") floatingMenuContentDiv.style.top = `${tailOffset + floatingMenuBounds.top}px`; + if (direction === "Top") floatingMenuContentDiv.style.bottom = `${tailOffset + floatingMenuBounds.bottom}px`; + if (direction === "Right") floatingMenuContentDiv.style.left = `${tailOffset + floatingMenuBounds.left}px`; + if (direction === "Left") floatingMenuContentDiv.style.right = `${tailOffset + floatingMenuBounds.right}px`; // Required to correctly position tail when scrolled (it has a `position: fixed` to prevent clipping) // We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever @@ -152,11 +155,11 @@ // We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever if (floatingMenuContentBounds.left - windowEdgeMargin <= workspaceBounds.left) { - floatingMenuContent.div().style.left = `${windowEdgeMargin}px`; + floatingMenuContentDiv.style.left = `${windowEdgeMargin}px`; if (workspaceBounds.left + floatingMenuContainerBounds.left === 12) zeroedBorderHorizontal = "Left"; } if (floatingMenuContentBounds.right + windowEdgeMargin >= workspaceBounds.right) { - floatingMenuContent.div().style.right = `${windowEdgeMargin}px`; + floatingMenuContentDiv.style.right = `${windowEdgeMargin}px`; if (workspaceBounds.right - floatingMenuContainerBounds.right === 12) zeroedBorderHorizontal = "Right"; } } @@ -165,11 +168,11 @@ // We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever if (floatingMenuContentBounds.top - windowEdgeMargin <= workspaceBounds.top) { - floatingMenuContent.div().style.top = `${windowEdgeMargin}px`; + floatingMenuContentDiv.style.top = `${windowEdgeMargin}px`; if (workspaceBounds.top + floatingMenuContainerBounds.top === 12) zeroedBorderVertical = "Top"; } if (floatingMenuContentBounds.bottom + windowEdgeMargin >= workspaceBounds.bottom) { - floatingMenuContent.div().style.bottom = `${windowEdgeMargin}px`; + floatingMenuContentDiv.style.bottom = `${windowEdgeMargin}px`; if (workspaceBounds.bottom - floatingMenuContainerBounds.bottom === 12) zeroedBorderVertical = "Bottom"; } } @@ -179,16 +182,16 @@ // We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever switch (`${zeroedBorderVertical}${zeroedBorderHorizontal}`) { case "TopLeft": - floatingMenuContent.div().style.borderTopLeftRadius = "0"; + floatingMenuContentDiv.style.borderTopLeftRadius = "0"; break; case "TopRight": - floatingMenuContent.div().style.borderTopRightRadius = "0"; + floatingMenuContentDiv.style.borderTopRightRadius = "0"; break; case "BottomLeft": - floatingMenuContent.div().style.borderBottomLeftRadius = "0"; + floatingMenuContentDiv.style.borderBottomLeftRadius = "0"; break; case "BottomRight": - floatingMenuContent.div().style.borderBottomRightRadius = "0"; + floatingMenuContentDiv.style.borderBottomRightRadius = "0"; break; default: break; @@ -196,7 +199,7 @@ } } - export function div(): HTMLDivElement { + export function div(): HTMLDivElement | undefined { return self; } @@ -217,7 +220,7 @@ // Measure the width of the floating menu content element, if it's currently visible // The result will be `undefined` if the menu is invisible, perhaps because an ancestor component is hidden with a falsy Svelte template if condition - const naturalWidth: number | undefined = floatingMenuContent?.div().clientWidth; + const naturalWidth: number | undefined = floatingMenuContent?.div()?.clientWidth; // Turn off measuring mode for the component, which triggers another call to the `afterUpdate()` Svelte event, so we can turn off the protection after that has happened measuringOngoing = false; @@ -365,7 +368,7 @@ function isPointerEventOutsideFloatingMenu(e: PointerEvent, extraDistanceAllowed = 0): boolean { // Consider all child menus as well as the top-level one - const allContainedFloatingMenus = [...self.querySelectorAll("[data-floating-menu-content]")]; + const allContainedFloatingMenus = [...(self?.querySelectorAll("[data-floating-menu-content]") || [])]; return !allContainedFloatingMenus.find((element) => !isPointerEventOutsideMenuElement(e, element, extraDistanceAllowed)); } diff --git a/frontend/src/components/layout/LayoutCol.svelte b/frontend/src/components/layout/LayoutCol.svelte index d3313bbb..50b58773 100644 --- a/frontend/src/components/layout/LayoutCol.svelte +++ b/frontend/src/components/layout/LayoutCol.svelte @@ -9,7 +9,7 @@ export let scrollableX = false; export let scrollableY = false; - let self: HTMLDivElement; + let self: HTMLDivElement | undefined; $: extraClasses = Object.entries(classes) .flatMap((classAndState) => (classAndState[1] ? [classAndState[0]] : [])) @@ -18,7 +18,7 @@ .flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : [])) .join(" "); - export function div(): HTMLDivElement { + export function div(): HTMLDivElement | undefined { return self; } diff --git a/frontend/src/components/layout/LayoutRow.svelte b/frontend/src/components/layout/LayoutRow.svelte index 97e0cb80..b8f11d44 100644 --- a/frontend/src/components/layout/LayoutRow.svelte +++ b/frontend/src/components/layout/LayoutRow.svelte @@ -9,7 +9,7 @@ export let scrollableX = false; export let scrollableY = false; - let self: HTMLDivElement; + let self: HTMLDivElement | undefined; $: extraClasses = Object.entries(classes) .flatMap((classAndState) => (classAndState[1] ? [classAndState[0]] : [])) @@ -18,7 +18,7 @@ .flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : [])) .join(" "); - export function div(): HTMLDivElement { + export function div(): HTMLDivElement | undefined { return self; } diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index b9f6286d..87a8ac17 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -28,9 +28,9 @@ import type { Editor } from "@/wasm-communication/editor"; import type { DocumentState } from "@/state-providers/document"; - let rulerHorizontal: CanvasRuler; - let rulerVertical: CanvasRuler; - let canvasContainer: HTMLDivElement; + let rulerHorizontal: CanvasRuler | undefined; + let rulerVertical: CanvasRuler | undefined; + let canvasContainer: HTMLDivElement | undefined; const editor = getContext("editor"); const document = getContext("document"); @@ -125,8 +125,8 @@ await tick(); if (textInput) { - const foreignObject = canvasContainer.getElementsByTagName("foreignObject")[0] as SVGForeignObjectElement; - if (foreignObject.children.length > 0) return; + const foreignObject = canvasContainer?.getElementsByTagName("foreignObject")[0] as SVGForeignObjectElement | undefined; + if (!foreignObject || foreignObject.children.length > 0) return; const addedInput = foreignObject.appendChild(textInput); window.dispatchEvent(new CustomEvent("modifyinputfield", { detail: addedInput })); @@ -282,6 +282,8 @@ // Resize elements to render the new viewport size export function viewportResize() { + if (!canvasContainer) return; + // Resize the canvas canvasSvgWidth = Math.ceil(parseFloat(getComputedStyle(canvasContainer).width)); canvasSvgHeight = Math.ceil(parseFloat(getComputedStyle(canvasContainer).height)); diff --git a/frontend/src/components/panels/LayerTree.svelte b/frontend/src/components/panels/LayerTree.svelte index 7cea0919..51f6981d 100644 --- a/frontend/src/components/panels/LayerTree.svelte +++ b/frontend/src/components/panels/LayerTree.svelte @@ -29,7 +29,7 @@ entry: LayerPanelEntry; }; - let list: LayoutCol; + let list: LayoutCol | undefined; const RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT = 20; const LAYER_INDENT = 16; @@ -105,7 +105,7 @@ await tick(); - const textInput = (list?.div().querySelector("[data-text-input]:not([disabled])") || undefined) as HTMLInputElement | undefined; + const textInput = (list?.div()?.querySelector("[data-text-input]:not([disabled])") || undefined) as HTMLInputElement | undefined; textInput?.select(); } @@ -153,8 +153,8 @@ } function calculateDragIndex(tree: LayoutCol, clientY: number, select?: () => void): DraggingData { - const treeChildren = tree.div().children; - const treeOffset = tree.div().getBoundingClientRect().top; + const treeChildren = tree.div()?.children; + const treeOffset = tree.div()?.getBoundingClientRect().top; // Closest distance to the middle of the row along the Y axis let closest = Infinity; @@ -171,45 +171,47 @@ let markerHeight = 0; let previousHeight = undefined as undefined | number; - Array.from(treeChildren).forEach((treeChild, index) => { - const layerComponents = treeChild.getElementsByClassName("layer"); - if (layerComponents.length !== 1) return; - const child = layerComponents[0]; + if (treeChildren !== undefined && treeOffset !== undefined) { + Array.from(treeChildren).forEach((treeChild, index) => { + const layerComponents = treeChild.getElementsByClassName("layer"); + if (layerComponents.length !== 1) return; + const child = layerComponents[0]; - const indexAttribute = child.getAttribute("data-index"); - if (!indexAttribute) return; - const { folderIndex, entry: layer } = layers[parseInt(indexAttribute, 10)]; + const indexAttribute = child.getAttribute("data-index"); + if (!indexAttribute) return; + const { folderIndex, entry: layer } = layers[parseInt(indexAttribute, 10)]; - const rect = child.getBoundingClientRect(); - const position = rect.top + rect.height / 2; - const distance = position - clientY; + const rect = child.getBoundingClientRect(); + const position = rect.top + rect.height / 2; + const distance = position - clientY; - // Inserting above current row - if (distance > 0 && distance < closest) { - insertFolder = layer.path.slice(0, layer.path.length - 1); - insertIndex = folderIndex; - highlightFolder = false; - closest = distance; - markerHeight = previousHeight || treeOffset + INSERT_MARK_OFFSET; - } - // Inserting below current row - else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0) { - insertFolder = layer.layerType === "Folder" ? layer.path : layer.path.slice(0, layer.path.length - 1); - insertIndex = layer.layerType === "Folder" ? 0 : folderIndex + 1; - highlightFolder = layer.layerType === "Folder"; - closest = -distance; - markerHeight = index === treeChildren.length - 1 ? rect.bottom - INSERT_MARK_OFFSET : rect.bottom; - } - // Inserting with no nesting at the end of the panel - else if (closest === Infinity) { - if (layer.path.length === 1) insertIndex = folderIndex + 1; + // Inserting above current row + if (distance > 0 && distance < closest) { + insertFolder = layer.path.slice(0, layer.path.length - 1); + insertIndex = folderIndex; + highlightFolder = false; + closest = distance; + markerHeight = previousHeight || treeOffset + INSERT_MARK_OFFSET; + } + // Inserting below current row + else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0) { + insertFolder = layer.layerType === "Folder" ? layer.path : layer.path.slice(0, layer.path.length - 1); + insertIndex = layer.layerType === "Folder" ? 0 : folderIndex + 1; + highlightFolder = layer.layerType === "Folder"; + closest = -distance; + markerHeight = index === treeChildren.length - 1 ? rect.bottom - INSERT_MARK_OFFSET : rect.bottom; + } + // Inserting with no nesting at the end of the panel + else if (closest === Infinity) { + if (layer.path.length === 1) insertIndex = folderIndex + 1; - markerHeight = rect.bottom - INSERT_MARK_OFFSET; - } - previousHeight = rect.bottom; - }); + markerHeight = rect.bottom - INSERT_MARK_OFFSET; + } + previousHeight = rect.bottom; + }); + } - markerHeight -= treeOffset; + markerHeight -= (treeOffset || 0); return { select, diff --git a/frontend/src/components/panels/NodeGraph.svelte b/frontend/src/components/panels/NodeGraph.svelte index 0762cab4..19ce9761 100644 --- a/frontend/src/components/panels/NodeGraph.svelte +++ b/frontend/src/components/panels/NodeGraph.svelte @@ -22,9 +22,10 @@ const editor = getContext("editor"); const nodeGraph = getContext("nodeGraph"); - let graph: LayoutRow; - let nodesContainer: HTMLDivElement; - let nodeSearchInput: TextInput; + let graph: LayoutRow | undefined; + let nodesContainer: HTMLDivElement | undefined; + let nodeSearchInput: TextInput | undefined; + let transform = { scale: 1, x: 0, y: 0 }; let panning = false; let selected: bigint[] = []; @@ -75,7 +76,7 @@ } function createLinkPathInProgress(linkInProgressFromConnector?: HTMLDivElement, linkInProgressToConnector?: HTMLDivElement | DOMRect): [string, string] | undefined { - if (linkInProgressFromConnector && linkInProgressToConnector) { + if (linkInProgressFromConnector && linkInProgressToConnector && nodesContainer) { return createWirePath(linkInProgressFromConnector, linkInProgressToConnector, false, false); } return undefined; @@ -107,9 +108,12 @@ async function refreshLinks(): Promise { await tick(); + if (!nodesContainer) return; + const theNodesContainer = nodesContainer; + const links = $nodeGraph.links; nodeLinkPaths = links.flatMap((link, index) => { - const { nodePrimaryInput, nodePrimaryOutput } = resolveLink(link, nodesContainer); + const { nodePrimaryInput, nodePrimaryOutput } = resolveLink(link, theNodesContainer); if (!nodePrimaryInput || !nodePrimaryOutput) return []; if (disconnecting?.linkIndex === index) return []; @@ -129,6 +133,8 @@ } function buildWirePathLocations(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): { x: number; y: number }[] { + if (!nodesContainer) return []; + const containerBounds = nodesContainer.getBoundingClientRect(); const outX = verticalOut ? outputBounds.x + outputBounds.width / 2 : outputBounds.x + outputBounds.width - 1; @@ -193,7 +199,9 @@ let zoomFactor = 1 + Math.abs(scrollY) * WHEEL_RATE; if (scrollY > 0) zoomFactor = 1 / zoomFactor; - const { x, y, width, height } = graph.div().getBoundingClientRect(); + const bounds = graph?.div()?.getBoundingClientRect(); + if (!bounds) return; + const { x, y, width, height } = bounds; transform.scale *= zoomFactor; @@ -238,14 +246,15 @@ // Create the add node popup on right click, then exit if (rmb) { - const graphBounds = graph.div().getBoundingClientRect(); + const graphBounds = graph?.div()?.getBoundingClientRect(); + if (!graphBounds) return; nodeListLocation = { x: Math.round(((e.clientX - graphBounds.x) / transform.scale - transform.x) / GRID_SIZE), y: Math.round(((e.clientY - graphBounds.y) / transform.scale - transform.y) / GRID_SIZE), }; // Find actual relevant child and focus it (setTimeout is required to actually focus the input element) - setTimeout(() => nodeSearchInput.focus(), 0); + setTimeout(() => nodeSearchInput?.focus(), 0); document.addEventListener("keydown", keydown); return; @@ -277,9 +286,9 @@ const inputIndexInt = BigInt(inputIndex); const links = $nodeGraph.links; const linkIndex = links.findIndex((value) => value.linkEnd === nodeIdInt && value.linkEndInputIndex === inputIndexInt); - const nodeOutputConnectors = nodesContainer.querySelectorAll(`[data-node="${String(links[linkIndex].linkStart)}"] [data-port="output"]`) || undefined; + const nodeOutputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkStart)}"] [data-port="output"]`) || undefined; linkInProgressFromConnector = nodeOutputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as HTMLDivElement | undefined; - const nodeInputConnectors = nodesContainer.querySelectorAll(`[data-node="${String(links[linkIndex].linkEnd)}"] [data-port="input"]`) || undefined; + const nodeInputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkEnd)}"] [data-port="input"]`) || undefined; linkInProgressToConnector = nodeInputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as HTMLDivElement | undefined; disconnecting = { nodeId: nodeIdInt, inputIndex, linkIndex }; refreshLinks(); @@ -400,24 +409,26 @@ // Check if this node should be inserted between two other nodes if (selected.length === 1) { const selectedNodeId = selected[0]; - const selectedNode = nodesContainer.querySelector(`[data-node="${String(selectedNodeId)}"]`); + const selectedNode = nodesContainer?.querySelector(`[data-node="${String(selectedNodeId)}"]`) || undefined; // Check that neither the input or output of the selected node are already connected. const notConnected = $nodeGraph.links.findIndex((link) => link.linkStart === selectedNodeId || (link.linkEnd === selectedNodeId && link.linkEndInputIndex === BigInt(0))) === -1; - const input = selectedNode?.querySelector(`[data-port="input"]`); - const output = selectedNode?.querySelector(`[data-port="output"]`); + const input = selectedNode?.querySelector(`[data-port="input"]`) || undefined; + const output = selectedNode?.querySelector(`[data-port="output"]`) || undefined; // TODO: Make sure inputs are correctly typed - if (selectedNode && notConnected && input && output) { + if (selectedNode && notConnected && input && output && nodesContainer) { + const theNodesContainer = nodesContainer; + // Find the link that the node has been dragged on top of const link = $nodeGraph.links.find((link): boolean => { - const { nodePrimaryInput, nodePrimaryOutput } = resolveLink(link, nodesContainer); + const { nodePrimaryInput, nodePrimaryOutput } = resolveLink(link, theNodesContainer); if (!nodePrimaryInput || !nodePrimaryOutput) return false; const wireCurveLocations = buildWirePathLocations(nodePrimaryOutput.getBoundingClientRect(), nodePrimaryInput.getBoundingClientRect(), false, false); const selectedNodeBounds = selectedNode.getBoundingClientRect(); - const containerBoundsBounds = nodesContainer.getBoundingClientRect(); + const containerBoundsBounds = theNodesContainer.getBoundingClientRect(); return editor.instance.rectangleIntersects( new Float64Array(wireCurveLocations.map((loc) => loc.x)), @@ -428,6 +439,7 @@ selectedNodeBounds.right - containerBoundsBounds.x ); }); + // If the node has been dragged on top of the link then connect it into the middle. if (link) { editor.instance.connectNodesByLink(link.linkStart, 0, selectedNodeId, 0); diff --git a/frontend/src/components/widgets/inputs/CheckboxInput.svelte b/frontend/src/components/widgets/inputs/CheckboxInput.svelte index 8d5ca0e2..1535a5e3 100644 --- a/frontend/src/components/widgets/inputs/CheckboxInput.svelte +++ b/frontend/src/components/widgets/inputs/CheckboxInput.svelte @@ -14,7 +14,8 @@ export let icon: IconName = "Checkmark"; export let tooltip: string | undefined = undefined; - let inputElement: HTMLInputElement; + let inputElement: HTMLInputElement | undefined; + let id = `${Math.random()}`.substring(2); $: displayIcon = (!checked && icon === "Checkmark" ? "Empty12px" : icon) as IconName; @@ -23,7 +24,7 @@ return checked; } - export function input(): HTMLInputElement { + export function input(): HTMLInputElement | undefined { return inputElement; } @@ -35,7 +36,7 @@ - dispatch("checked", inputElement.checked)} {disabled} tabindex={disabled ? -1 : 0} bind:this={inputElement} /> + dispatch("checked", inputElement?.checked)} {disabled} tabindex={disabled ? -1 : 0} bind:this={inputElement} />