Svelte: Fix calling DOM API functions on the actual elements

This commit is contained in:
Keavon Chambers 2023-01-13 17:17:38 -08:00
parent 0019340096
commit fb10e5194e
15 changed files with 162 additions and 96 deletions

View file

@ -321,7 +321,6 @@ impl NodeGraphMessageHandler {
disabled: network.disabled.contains(id),
})
}
log::debug!("Frontend Nodes:\n{:#?}\n\nLinks:\n{:#?}", nodes, links);
responses.push_back(FrontendMessage::UpdateNodeGraph { nodes, links }.into());
}
@ -423,7 +422,6 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &mut dyn Iterator<Item = &
input_node,
input_node_connector_index,
} => {
log::debug!("Connect primary output from node {output_node} to input of index {input_node_connector_index} on node {input_node}.");
let node_id = input_node;
let Some(network) = self.get_active_network(document) else {

View file

@ -11,7 +11,7 @@
const dialog = getContext<DialogState>("dialog");
let dialogModal: FloatingMenu;
let self: FloatingMenu;
export function dismiss() {
dialog.dismissDialog();
@ -19,12 +19,12 @@
onMount(() => {
// Focus the first button in the popup
const emphasizedOrFirstButton = (dialogModal.div().querySelector("[data-emphasized]") || dialogModal.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();
});
</script>
<FloatingMenu open={true} class="dialog-modal" type="Dialog" direction="Center" bind:this={dialogModal} data-dialog-modal>
<FloatingMenu open={true} class="dialog-modal" type="Dialog" direction="Center" bind:this={self} data-dialog-modal>
<LayoutRow>
<LayoutCol class="icon-column">
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->

View file

@ -11,7 +11,7 @@
import TextLabel from "@/components/widgets/labels/TextLabel.svelte";
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.svelte";
let floatingMenu: FloatingMenu;
let self: FloatingMenu;
let scroller: LayoutCol;
// emits: ["update:open", "update:activeEntry", "naturalWidth"],
@ -35,8 +35,8 @@
// Called only when `open` is changed from outside this component (with v-model)
$: watchOpen(open);
$: dispatch("open", isOpen);
$: watchEntries(entries, floatingMenu);
$: watchDrawIcon(drawIcon, floatingMenu);
$: watchEntries(entries, self);
$: watchDrawIcon(drawIcon, self);
$: virtualScrollingTotalHeight = entries.length === 0 ? 0 : entries[0].length * virtualScrollingEntryHeight;
$: virtualScrollingStartIndex = Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0;
$: virtualScrollingEndIndex = entries.length === 0 ? 0 : Math.min(entries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight);
@ -48,12 +48,12 @@
// TODO: Svelte: fix infinite loop and reenable
function watchEntries(_: MenuListEntry[][], floatingMenu: FloatingMenu) {
// floatingMenu?.measureAndEmitNaturalWidth();
// floatingMenu?.div().measureAndEmitNaturalWidth();
}
// TODO: Svelte: fix infinite loop and reenable
function watchDrawIcon(_: boolean, floatingMenu: FloatingMenu) {
// floatingMenu?.measureAndEmitNaturalWidth();
// floatingMenu?.div().measureAndEmitNaturalWidth();
}
function onScroll(e: Event) {
@ -198,7 +198,7 @@
{direction}
{minWidth}
scrollableY={scrollableY && virtualScrollingEntryHeight === 0}
bind:this={floatingMenu}
bind:this={self}
>
<!-- If we put the scrollableY on the layoutcol for non-font dropdowns then for some reason it always creates a tiny scrollbar.
However when we are using the virtual scrolling then we need the layoutcol to be scrolling so we can bind the events without using $refs. -->

View file

@ -132,10 +132,10 @@
// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
// We use `.style` on a ref (instead of a `:style` Vue binding) because the binding causes the `updated()` hook to call the function we're in recursively forever
const tailOffset = type === "Popover" ? 10 : 0;
if (direction === "Bottom") floatingMenuContent.style.top = `${tailOffset + floatingMenuBounds.top}px`;
if (direction === "Top") floatingMenuContent.style.bottom = `${tailOffset + floatingMenuBounds.bottom}px`;
if (direction === "Right") floatingMenuContent.style.left = `${tailOffset + floatingMenuBounds.left}px`;
if (direction === "Left") floatingMenuContent.style.right = `${tailOffset + floatingMenuBounds.right}px`;
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`;
// Required to correctly position tail when scrolled (it has a `position: fixed` to prevent clipping)
// We use `.style` on a ref (instead of a `:style` Vue binding) because the binding causes the `updated()` hook to call the function we're in recursively forever
@ -154,11 +154,11 @@
// We use `.style` on a ref (instead of a `:style` Vue binding) because the binding causes the `updated()` hook to call the function we're in recursively forever
if (floatingMenuContentBounds.left - windowEdgeMargin <= workspaceBounds.left) {
floatingMenuContent.style.left = `${windowEdgeMargin}px`;
floatingMenuContent.div().style.left = `${windowEdgeMargin}px`;
if (workspaceBounds.left + floatingMenuContainerBounds.left === 12) zeroedBorderHorizontal = "Left";
}
if (floatingMenuContentBounds.right + windowEdgeMargin >= workspaceBounds.right) {
floatingMenuContent.style.right = `${windowEdgeMargin}px`;
floatingMenuContent.div().style.right = `${windowEdgeMargin}px`;
if (workspaceBounds.right - floatingMenuContainerBounds.right === 12) zeroedBorderHorizontal = "Right";
}
}
@ -167,11 +167,11 @@
// We use `.style` on a ref (instead of a `:style` Vue binding) because the binding causes the `updated()` hook to call the function we're in recursively forever
if (floatingMenuContentBounds.top - windowEdgeMargin <= workspaceBounds.top) {
floatingMenuContent.style.top = `${windowEdgeMargin}px`;
floatingMenuContent.div().style.top = `${windowEdgeMargin}px`;
if (workspaceBounds.top + floatingMenuContainerBounds.top === 12) zeroedBorderVertical = "Top";
}
if (floatingMenuContentBounds.bottom + windowEdgeMargin >= workspaceBounds.bottom) {
floatingMenuContent.style.bottom = `${windowEdgeMargin}px`;
floatingMenuContent.div().style.bottom = `${windowEdgeMargin}px`;
if (workspaceBounds.bottom - floatingMenuContainerBounds.bottom === 12) zeroedBorderVertical = "Bottom";
}
}
@ -181,16 +181,16 @@
// We use `.style` on a ref (instead of a `:style` Vue binding) because the binding causes the `updated()` hook to call the function we're in recursively forever
switch (`${zeroedBorderVertical}${zeroedBorderHorizontal}`) {
case "TopLeft":
floatingMenuContent.style.borderTopLeftRadius = "0";
floatingMenuContent.div().style.borderTopLeftRadius = "0";
break;
case "TopRight":
floatingMenuContent.style.borderTopRightRadius = "0";
floatingMenuContent.div().style.borderTopRightRadius = "0";
break;
case "BottomLeft":
floatingMenuContent.style.borderBottomLeftRadius = "0";
floatingMenuContent.div().style.borderBottomLeftRadius = "0";
break;
case "BottomRight":
floatingMenuContent.style.borderBottomRightRadius = "0";
floatingMenuContent.div().style.borderBottomRightRadius = "0";
break;
default:
break;
@ -219,7 +219,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 `v-if` condition
const naturalWidth: number | undefined = floatingMenuContent?.clientWidth;
const naturalWidth: number | undefined = floatingMenuContent?.div().clientWidth;
// Turn off measuring mode for the component, which triggers another call to the `updated()` Vue event, so we can turn off the protection after that has happened
measuringOngoing = false;

View file

@ -9,7 +9,7 @@
export let scrollableX: boolean = false;
export let scrollableY: boolean = false;
let divElement: HTMLDivElement;
let self: HTMLDivElement;
$: extraClasses = Object.entries(classes)
.flatMap((classAndState) => (classAndState[1] ? [classAndState[0]] : []))
@ -19,7 +19,7 @@
.join(" ");
export function div(): HTMLDivElement {
return divElement;
return self;
}
</script>
@ -29,21 +29,52 @@
class:scrollable-y={scrollableY}
style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip}
bind:this={divElement}
bind:this={self}
on:focus
on:blur
on:fullscreenchange
on:fullscreenerror
on:scroll
on:cut
on:copy
on:paste
on:keydown
on:keypress
on:keyup
on:auxclick
on:click
on:contextmenu
on:dblclick
on:mousedown
on:mouseenter
on:mouseleave
on:mousemove
on:mouseover
on:mouseout
on:mouseup
on:select
on:wheel
on:drag
on:dragend
on:dragenter
on:dragstart
on:dragleave
on:dragover
on:drop
on:touchcancel
on:touchend
on:touchmove
on:touchstart
on:pointerover
on:pointerenter
on:pointerdown
on:pointermove
on:pointerup
on:dragleave
on:dragover
on:dragstart
on:dragend
on:drop
on:wheel
on:scroll
on:focus
on:blur
on:pointercancel
on:pointerout
on:pointerleave
on:gotpointercapture
on:lostpointercapture
{...$$restProps}
>
<slot />

View file

@ -9,7 +9,7 @@
export let scrollableX: boolean = false;
export let scrollableY: boolean = false;
let divElement: HTMLDivElement;
let self: HTMLDivElement;
$: extraClasses = Object.entries(classes)
.flatMap((classAndState) => (classAndState[1] ? [classAndState[0]] : []))
@ -19,7 +19,7 @@
.join(" ");
export function div(): HTMLDivElement {
return divElement;
return self;
}
</script>
@ -29,21 +29,52 @@
class:scrollable-y={scrollableY}
style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip}
bind:this={divElement}
bind:this={self}
on:focus
on:blur
on:fullscreenchange
on:fullscreenerror
on:scroll
on:cut
on:copy
on:paste
on:keydown
on:keypress
on:keyup
on:auxclick
on:click
on:contextmenu
on:dblclick
on:mousedown
on:mouseenter
on:mouseleave
on:mousemove
on:mouseover
on:mouseout
on:mouseup
on:select
on:wheel
on:drag
on:dragend
on:dragenter
on:dragstart
on:dragleave
on:dragover
on:drop
on:touchcancel
on:touchend
on:touchmove
on:touchstart
on:pointerover
on:pointerenter
on:pointerdown
on:pointermove
on:pointerup
on:dragleave
on:dragover
on:dragstart
on:dragend
on:drop
on:wheel
on:scroll
on:focus
on:blur
on:pointercancel
on:pointerout
on:pointerleave
on:gotpointercapture
on:lostpointercapture
{...$$restProps}
>
<slot />

View file

@ -28,10 +28,9 @@
import { type Editor } from "@/wasm-communication/editor";
import { type DocumentState } from "@/state-providers/document";
let self: LayoutCol;
let rulerHorizontal: CanvasRuler;
let rulerVertical: CanvasRuler;
let canvasDiv: HTMLDivElement;
let canvasContainer: HTMLDivElement;
const editor = getContext<Editor>("editor");
const document = getContext<DocumentState>("document");
@ -116,7 +115,7 @@
function canvasPointerDown(e: PointerEvent) {
const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable;
if (!onEditbox) canvasDiv?.setPointerCapture(e.pointerId);
if (!onEditbox) canvasContainer?.setPointerCapture(e.pointerId);
}
// Update rendered SVGs
@ -127,7 +126,7 @@
await tick();
if (textInput) {
const foreignObject = canvasDiv.getElementsByTagName("foreignObject")[0] as SVGForeignObjectElement;
const foreignObject = canvasContainer.getElementsByTagName("foreignObject")[0] as SVGForeignObjectElement;
if (foreignObject.children.length > 0) return;
const addedInput = foreignObject.appendChild(textInput);
@ -285,8 +284,8 @@
// Resize elements to render the new viewport size
export function viewportResize() {
// Resize the canvas
canvasSvgWidth = Math.ceil(parseFloat(getComputedStyle(canvasDiv).width));
canvasSvgHeight = Math.ceil(parseFloat(getComputedStyle(canvasDiv).height));
canvasSvgWidth = Math.ceil(parseFloat(getComputedStyle(canvasContainer).width));
canvasSvgHeight = Math.ceil(parseFloat(getComputedStyle(canvasContainer).height));
// Resize the rulers
rulerHorizontal?.resize();
@ -382,7 +381,7 @@
});
</script>
<LayoutCol class="document" bind:this={self}>
<LayoutCol class="document">
<LayoutRow class="options-bar" scrollableX={true}>
<WidgetLayout layout={$document.documentModeLayout} />
<WidgetLayout layout={$document.toolOptionsLayout} />
@ -422,7 +421,7 @@
y={cursorTop}
/>
{/if}
<div class="canvas" on:pointerdown={(e) => canvasPointerDown(e)} on:dragover={(e) => e.preventDefault()} on:drop={(e) => pasteFile(e)} bind:this={canvasDiv} data-canvas>
<div class="canvas" on:pointerdown={(e) => canvasPointerDown(e)} on:dragover={(e) => e.preventDefault()} on:drop={(e) => pasteFile(e)} bind:this={canvasContainer} data-canvas>
<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
{@html artboardSvg}
</svg>

View file

@ -109,7 +109,7 @@
await tick();
const textInput: HTMLInputElement | undefined = list?.querySelector("[data-text-input]:not([disabled])") || undefined;
const textInput = (list?.div().querySelector("[data-text-input]:not([disabled])") || undefined) as HTMLInputElement | undefined;
textInput?.select();
}
@ -169,7 +169,7 @@
function calculateDragIndex(tree: LayoutCol, clientY: number, select?: () => void): DraggingData {
const treeChildren = tree.div().children;
const treeOffset = tree.getBoundingClientRect().top;
const treeOffset = tree.div().getBoundingClientRect().top;
// Closest distance to the middle of the row along the Y axis
let closest = Infinity;

View file

@ -180,7 +180,7 @@
let zoomFactor = 1 + Math.abs(scrollY) * WHEEL_RATE;
if (scrollY > 0) zoomFactor = 1 / zoomFactor;
const { x, y, width, height } = graph.getBoundingClientRect();
const { x, y, width, height } = graph.div().getBoundingClientRect();
transform.scale *= zoomFactor;
@ -222,7 +222,7 @@
// Handle the add node popup on right click
if (e.button === 2) {
const graphBounds = graph.getBoundingClientRect();
const graphBounds = graph.div().getBoundingClientRect();
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),

View file

@ -28,7 +28,7 @@
export let sharpRightCorners = false;
export let placeholder: string | undefined = undefined;
let input: HTMLInputElement | HTMLTextAreaElement;
let inputOrTextarea: HTMLInputElement | HTMLTextAreaElement;
let id = `${Math.random()}`.substring(2);
let macKeyboardLayout = platformIsMac();
let inputValue = value;
@ -40,20 +40,28 @@
export function selectAllText(currentText: string) {
// Setting the value directly is required to make `input.select()` work
// TODO: Svelte: Test if the above message is still true
input.value = currentText;
input.select();
inputOrTextarea.value = currentText;
inputOrTextarea.select();
}
export function focus() {
inputOrTextarea.focus();
}
export function unFocus() {
input.blur();
inputOrTextarea.blur();
}
export function getInputElementValue(): string {
return input.value;
export function getValue(): string {
return inputOrTextarea.value;
}
export function setInputElementValue(value: string) {
input.value = value;
inputOrTextarea.value = value;
}
export function element(): HTMLInputElement | HTMLTextAreaElement {
return inputOrTextarea;
}
</script>
@ -68,7 +76,7 @@
{disabled}
{placeholder}
bind:value={inputValue}
bind:this={input}
bind:this={inputOrTextarea}
on:focus={() => dispatch("textFocused")}
on:blur={() => dispatch("textChanged")}
on:change={() => dispatch("textChanged")}
@ -85,7 +93,7 @@
{spellcheck}
{disabled}
bind:value={inputValue}
bind:this={input}
bind:this={inputOrTextarea}
on:focus={() => dispatch("textFocused")}
on:blur={() => dispatch("textChanged")}
on:change={() => dispatch("textChanged")}

View file

@ -51,7 +51,7 @@
export let incrementCallbackIncrease: (() => void) | undefined = undefined;
export let incrementCallbackDecrease: (() => void) | undefined = undefined;
let fieldInput: FieldInput;
let self: FieldInput;
let text = displayText(value);
let editing = false;
// Stays in sync with a binding to the actual input range slider element.
@ -127,7 +127,7 @@
function sliderPointerUp() {
// User clicked but didn't drag, so we focus the text input element
if (rangeSliderClickDragState === "mousedown") {
const inputElement = fieldInput.querySelector("[data-input-element]") as HTMLInputElement | undefined;
const inputElement = self.element().querySelector("[data-input-element]") as HTMLInputElement | undefined;
if (!inputElement) return;
// Set the slider position back to the original position to undo the user moving it
@ -148,7 +148,7 @@
editing = true;
fieldInput.selectAllText(text);
self.selectAllText(text);
}
// Called only when `value` is changed from the <input> element via user input and committed, either with the
@ -164,7 +164,7 @@
editing = false;
fieldInput.unFocus();
self.unFocus();
}
function onCancelTextChange() {
@ -172,7 +172,7 @@
editing = false;
fieldInput.unFocus();
self.unFocus();
}
function onIncrement(direction: "Decrease" | "Increase") {
@ -245,7 +245,7 @@
{sharpRightCorners}
spellcheck={false}
styles={{ "min-width": minWidth > 0 ? `${minWidth}px` : undefined, "--progress-factor": (rangeSliderValueAsRendered - rangeMin) / (rangeMax - rangeMin) }}
bind:this={fieldInput}
bind:this={self}
>
{#if value !== undefined && mode === "Increment" && incrementBehavior !== "None"}
<button class="arrow left" on:click={() => onIncrement("Decrease")} tabindex="-1" />

View file

@ -1,24 +1,17 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { type IconName } from "@/utility-functions/icons";
import LayoutRow from "@/components/layout/LayoutRow.svelte";
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.svelte";
// emits: ["update:checked"],
const dispatch = createEventDispatcher<{ checked: boolean }>();
export let checked: boolean;
export let disabled = false;
export let icon: IconName = "Checkmark";
export let tooltip: string | undefined = undefined;
let checkboxInput: CheckboxInput;
</script>
<LayoutRow class="optional-input" classes={{ disabled }}>
<CheckboxInput {checked} on:checked {disabled} {icon} {tooltip} bind:this={checkboxInput} />
<CheckboxInput {checked} on:checked {disabled} {icon} {tooltip} />
</LayoutRow>
<style lang="scss" global>

View file

@ -11,7 +11,7 @@
export let tooltip: string | undefined = undefined;
export let disabled = false;
let fieldInput: FieldInput;
let self: FieldInput;
let editing = false;
let inputValue = value;
@ -30,16 +30,20 @@
onCancelTextChange();
// TODO: Find a less hacky way to do this
dispatch("commitText", fieldInput.getInputElementValue());
dispatch("commitText", self.getValue());
// Required if value is not changed by the parent component upon update:value event
fieldInput.setInputElementValue(value);
self.setInputElementValue(value);
}
function onCancelTextChange() {
editing = false;
fieldInput.unFocus();
self.unFocus();
}
export function focus() {
self.focus();
}
</script>
@ -59,7 +63,7 @@
{disabled}
{tooltip}
value={inputValue}
bind:this={fieldInput}
bind:this={self}
/>
<style lang="scss" global>

View file

@ -19,7 +19,7 @@
export let minWidth = 0;
export let sharpRightCorners = false;
let fieldInput: FieldInput;
let self: FieldInput;
let editing = false;
let text = value;
@ -28,7 +28,7 @@
function onTextFocused() {
editing = true;
fieldInput.selectAllText(text);
self.selectAllText(text);
}
// Called only when `value` is changed from the <input> element via user input and committed, either with the
@ -40,16 +40,20 @@
onCancelTextChange();
// TODO: Find a less hacky way to do this
dispatch("commitText", fieldInput.getInputElementValue());
dispatch("commitText", self.getValue());
// Required if value is not changed by the parent component upon update:value event
fieldInput.setInputElementValue(value);
self.setInputElementValue(value);
}
function onCancelTextChange() {
editing = false;
fieldInput.unFocus();
self.unFocus();
}
export function focus() {
self.focus();
}
</script>
@ -68,7 +72,7 @@
{tooltip}
{placeholder}
{sharpRightCorners}
bind:this={fieldInput}
bind:this={self}
/>
<style lang="scss" global>

View file

@ -115,8 +115,6 @@ fn handle_message(message: String) -> String {
let serialized = ron::to_string(&send_frontend_message_to_js(response.clone())).unwrap();
if let Err(error) = ron::from_str::<FrontendMessage>(&serialized) {
log::error!("Error deserializing message: {}", error);
log::debug!("{:#?}", response);
log::debug!("{}", serialized);
}
}