From 9bd319fba45c586e1c9f6fe543046940c3c38772 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 10 Mar 2023 03:47:06 -0800 Subject: [PATCH] Svelte bug fixes and code cleanup (#1074) * Fix middle click tab close * Fix swapping colors in the ColorPicker * Cleanup * Fix field inputs * Fix tab key-based navigation * Fix ColorPicker reseting Initial color when opened * Prevent backend fighting with user dragging NumberInput * Remaining fixes and comment cleanup --- frontend-svelte/.eslintrc.js | 2 +- frontend-svelte/README.md | 25 ++++++---- frontend-svelte/src/README.md | 28 ++++++----- frontend-svelte/src/components/Editor.svelte | 9 ++-- frontend-svelte/src/components/README.md | 44 ++++++++--------- .../floating-menus/ColorPicker.svelte | 47 +++++++++++-------- .../components/floating-menus/MenuList.svelte | 4 +- .../src/components/layout/FloatingMenu.svelte | 24 +++++----- .../src/components/panels/NodeGraph.svelte | 3 +- .../src/components/widgets/WidgetRow.svelte | 2 +- .../widgets/inputs/ColorInput.svelte | 11 ++++- .../widgets/inputs/DropdownInput.svelte | 4 +- .../widgets/inputs/FieldInput.svelte | 4 +- .../widgets/inputs/MenuBarInput.svelte | 4 +- .../widgets/inputs/NumberInput.svelte | 5 +- .../widgets/inputs/SwatchPairInput.svelte | 22 ++++++++- .../widgets/inputs/TextAreaInput.svelte | 14 ++---- .../widgets/inputs/TextInput.svelte | 11 ++--- .../widgets/labels/IconLabel.svelte | 2 - .../components/window/workspace/Panel.svelte | 23 +++++++-- frontend-svelte/src/io-managers/input.ts | 43 ++++++++--------- frontend-svelte/src/main.ts | 2 + frontend-svelte/src/state-providers/fonts.ts | 2 +- .../src/utility-functions/platform.ts | 16 +++++++ .../src/wasm-communication/editor.ts | 25 +++++----- frontend-svelte/webpack.config.ts | 25 ---------- 26 files changed, 221 insertions(+), 180 deletions(-) diff --git a/frontend-svelte/.eslintrc.js b/frontend-svelte/.eslintrc.js index 4d774966..893f3831 100644 --- a/frontend-svelte/.eslintrc.js +++ b/frontend-svelte/.eslintrc.js @@ -95,7 +95,7 @@ module.exports = { "newlines-between": "always-and-inside-groups", pathGroups: [ { - pattern: "**/*.vue", + pattern: "**/*.svelte", group: "unknown", position: "after", }, diff --git a/frontend-svelte/README.md b/frontend-svelte/README.md index 7604ae19..30db98c3 100644 --- a/frontend-svelte/README.md +++ b/frontend-svelte/README.md @@ -1,34 +1,41 @@ # Overview of `/frontend/` -The Graphite frontend is a web app that provides the presentation for the editor. It displays the GUI based on state from the backend and provides users with interactive widgets that send updates to the backend, which is the source of truth for state information. The frontend is built out of reactive components using the [Vue](https://vuejs.org/) framework. The backend is written in Rust and compiled to WebAssembly (WASM) to be run in the browser alongside the JS code. + +The Graphite frontend is a web app that provides the presentation for the editor. It displays the GUI based on state from the backend and provides users with interactive widgets that send updates to the backend, which is the source of truth for state information. The frontend is built out of reactive components using the [Svelte](https://svelte.dev/) framework. The backend is written in Rust and compiled to WebAssembly (WASM) to be run in the browser alongside the JS code. For lack of other options, the frontend is currently written as a web app. Maintaining web compatibility will always be a requirement, but the long-term plan is to port this code to a Rust-based native GUI framework, either written by the Rust community or created by our project if necessary. As a medium-term compromise, we may wrap the web-based frontend in a desktop webview windowing solution like Electron (probably not) or [Tauri](https://tauri.studio/) (probably). ## Bundled assets: `assets/` + Icons and images that are used in components and embedded into the application bundle by the build system using [loaders](https://webpack.js.org/loaders/). ## Public assets: `public/` + Static content like favicons that are copied directly into the root of the build output by the build system. -## Vue/TypeScript source: `src/` -Source code for the web app in the form of Vue components and [TypeScript](https://www.typescriptlang.org/) files. +## Svelte/TypeScript source: `src/` + +Source code for the web app in the form of Svelte components and [TypeScript](https://www.typescriptlang.org/) files. ## WebAssembly wrapper: `wasm/` + Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for the web app to use unburdened by Rust's complex data types that are incompatible with JS data types. Bindings (JS functions that call into the WASM module) are provided by [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/) in concert with [wasm-pack](https://github.com/rustwasm/wasm-pack). ## ESLint configurations: `.eslintrc.js` -[ESLint](https://eslint.org/) is the tool which enforces style rules on the JS, TS, and Vue files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when (in VS Code) the file is saved or `npm run lint` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Vue support, [Airbnb](https://github.com/airbnb/javascript)'s popular catalog of sane defaults, and [Prettier](https://prettier.io/)'s role as a code formatter. + +[ESLint](https://eslint.org/) is the tool which enforces style rules on the JS, TS, and Svelte files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when (in VS Code) the file is saved or `npm run lint` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support, [Airbnb](https://github.com/airbnb/javascript)'s popular catalog of sane defaults, and [Prettier](https://prettier.io/)'s role as a code formatter. ## npm ecosystem packages: `package.json` -While we don't use Node.js as a JS-based server, we do have to rely on its wide ecosystem of packages for our build system toolchain. If you're just getting started, make sure to install the latest LTS copy of Node.js and then run `cd frontend && npm install` to install these packages on your system. Our project's philosophy on third-party packages is to keep our dependency tree as light as possible, so adding anything new to our `package.json` should have overwhelming justification. Most of the packages are just development tooling (TypeScript, Vue CLI, ESLint, Prettier, wasm-pack, and [Sass](https://sass-lang.com/)) that run in your console during the build process. + +While we don't use Node.js as a JS-based server, we do have to rely on its wide ecosystem of packages for our build system toolchain. If you're just getting started, make sure to install the latest LTS copy of Node.js and then run `cd frontend && npm install` to install these packages on your system. Our project's philosophy on third-party packages is to keep our dependency tree as light as possible, so adding anything new to our `package.json` should have overwhelming justification. Most of the packages are just development tooling (TypeScript, Webpack, ESLint, Prettier, wasm-pack, and [Sass](https://sass-lang.com/)) that run in your console during the build process. ## npm package installed versions: `package-lock.json` + Specifies the exact versions of packages installed in the npm dependency tree. While `package.json` specifies which packages to install and their minimum/maximum acceptable version numbers, `package-lock.json` represents the exact versions of each dependency and sub-dependency. Running `npm install` will grab these exact versions to ensure you are using the same packages as everyone else working on Graphite. `npm update` will modify `package-lock.json` to specify newer versions of any updated (sub-)dependencies and download those, as long as they don't exceed the maximum version allowed in `package.json`. To check for newer versions that exceed the max version, run `npm outdated` to see a list. Unless you know why you are doing it, try to avoid committing updates to `package-lock.json` by mistake if your code changes don't pertain to package updates. And never manually modify the file. ## TypeScript configurations: `tsconfig.json` + Basic configuration options for the TypeScript build tool to do its job in our repository. -## vue-svg-loader.js -An extremely simple Webpack loader that allows us to `import` SVG files into our JS to be used like they are Vue components. They end up as inline SVG elements in the web page like `...`, rather than being `` elements, which provides some benefits like being able to apply CSS styles to them. These get embedded into the bundle (they live somewhere all together in a big, messy JS file) rather than being separate static SVG files that would have to be served individually. +## Webpack configurations: `webpack.config.js` -## Vue CLI/Webpack configurations: `vue.config.js` -[Vue CLI](https://cli.vuejs.org/) is a command line tool built around the [Webpack](https://webpack.js.org/) bundler/build system. This file is where we configure Webpack to set up plugins (like wasm-pack and license-checker) and loaders (like for Vue and SVG files). Part of the license-checker plugin setup includes some functions to format web package licenses, as well as Rust package licenses provided by [cargo-about](https://github.com/EmbarkStudios/cargo-about), into a text file that's distributed with the application to provide license notices for third-party code. +We use the [Webpack](https://webpack.js.org/) bundler/build system. This file is where we configure Webpack to set up plugins (like wasm-pack and license-checker) and loaders (like for Svelte and SVG files). Part of the license-checker plugin setup includes some functions to format web package licenses, as well as Rust package licenses provided by [cargo-about](https://github.com/EmbarkStudios/cargo-about), into a text file that's distributed with the application to provide license notices for third-party code. diff --git a/frontend-svelte/src/README.md b/frontend-svelte/src/README.md index c2ef55e9..caaa591c 100644 --- a/frontend-svelte/src/README.md +++ b/frontend-svelte/src/README.md @@ -1,26 +1,26 @@ # Overview of `/frontend/src/` -## Vue components: `components/` +## Svelte components: `components/` -Vue components that build the Graphite editor GUI, which are mounted in `App.svelte`. These are Vue SFCs (single-file components) which each contain a Vue-templated HTML section, an SCSS (Stylus CSS) section, and a script section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur. +Svelte components that build the Graphite editor GUI, which are mounted in `App.svelte`. These each contain a Svelte-templated HTML section, an SCSS (Stylus CSS) section, and a script section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur. ## I/O managers: `io-managers/` TypeScript files which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to backend events to execute JS APIs, and in response to these APIs or user interactions, they may call functions into the backend (defined in `/frontend/wasm/editor_api.rs`). -Each I/O manager is a self-contained module where one instance is created in `App.svelte` when it's mounted to the DOM at app startup. +Each I/O manager is a self-contained module where one instance is created in `Editor.svelte` when it's mounted to the DOM at app startup. -During development when HMR (hot-module replacement) occurs, these are also unmounted to clean up after themselves, so they can be mounted again with the updated code. Therefore, any side-effects that these managers cause (e.g. adding event listeners to the page) need a destructor function that cleans them up. The destructor function, when applicable, is returned by the module and automatically called in `App.svelte` on unmount. +During development when HMR (hot-module replacement) occurs, these are also unmounted to clean up after themselves, so they can be mounted again with the updated code. Therefore, any side-effects that these managers cause (e.g. adding event listeners to the page) need a destructor function that cleans them up. The destructor function, when applicable, is returned by the module and automatically called in `Editor.svelte` on unmount. ## State providers: `state-providers/` -TypeScript files which provide reactive state and importable functions to Vue components. Each module defines a Vue reactive state object `const state = reactive({ ... });` and exports this from the module in the returned object as the key-value pair `state: readonly(state) as typeof state,` using Vue's `readonly()` wrapper. Other functions may also be defined in the module and exported after `state`, which provide a way for Vue components to call functions to manipulate the state. +TypeScript files which provide reactive state and importable functions to Svelte components. Each module defines a Svelte writable store `const { subscribe, update } = writable({ .. });` and exports the `subscribe` method from the module in the returned object. Other functions may also be defined in the module and exported after `subscribe`, which provide a way for Svelte components to call functions to manipulate the state. -In `App.svelte`, an instance of each of these are given to Vue's [`provide()`](https://vuejs.org/api/application.html#app-provide) function. This allows any component to access the state provider instance by specifying it in its `inject: [...]` array. The state is accessed in a component with `this.stateProviderName.state.someReactiveVariable` and any exposed functions are accessed with `this.stateProviderName.state.someExposedVariable()`. They can also be used in the Vue HTML template (sans the `this.` prefix). +In `Editor.svelte`, an instance of each of these are given to Svelte's [`setContext()`](https://svelte.dev/docs#run-time-svelte-setcontext) function. This allows any component to access the state provider instance using `const exampleStateProvider = getContext("exampleStateProvider");`. ## _I/O managers vs. state providers_ -_Some state providers, similarly to I/O managers, may subscribe to backend events, call functions from `editor_api.rs` into the backend, and interact with browser APIs and user input. The difference is that state providers are meant to be `inject`ed by components to use them for reactive state, while I/O managers are meant to be self-contained systems that operate for the lifetime of the application and aren't touched by Vue components._ +_Some state providers, similarly to I/O managers, may subscribe to backend events, call functions from `editor_api.rs` into the backend, and interact with browser APIs and user input. The difference is that state providers are meant to be made available to components via `getContext()` to use them for reactive state, while I/O managers are meant to be self-contained systems that operate for the lifetime of the application and aren't touched by Svelte components._ ## Utility functions: `utility-functions/` @@ -34,7 +34,7 @@ TypeScript files which serve as the JS interface to the WASM bindings for the ed Instantiates the WASM and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the WASM bindings JS module provided by wasm-bindgen/wasm-pack. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same WASM module instance. The function returns an object where `raw` is the WASM module, `instance` is the editor, and `subscriptions` is the subscription router (described below). -`initWasm()` occurs in `main.ts` right before the Vue application exists, then `createEditor()` is run in `App.svelte` during the Vue app's creation. Similarly to the state providers described above, the editor is `provide`d so other components can `inject` it and call functions on `this.editor.raw`, `this.editor.instance`, or `this.editor.subscriptions`. +`initWasm()` occurs in `main.ts` right before the Svelte application exists, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the state providers described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.raw`, `editor.instance`, or `editor.subscriptions`. ### Message definitions: `messages.ts` @@ -44,10 +44,14 @@ Defines the message formats and data types received from the backend. Since Rust Associates messages from the backend with subscribers in the frontend, and routes messages to subscriber callbacks. This module provides a `subscribeJsMessage(messageType, callback)` function which JS code throughout the frontend can call to be registered as the exclusive handler for a chosen message type. This file's other exported function, `handleJsMessage(messageType, messageData, wasm, instance)`, is called in `editor.ts` by the associated editor instance when the backend sends a `FrontendMessage`. When this occurs, the subscription router delivers the message to the subscriber for given `messageType` by executing its registered `callback` function. As an argument to the function, it provides the `messageData` payload transformed into its TypeScript-friendly format defined in `messages.ts`. -## Vue app: `App.svelte` +## Svelte app entry point: `App.svelte` -The entry point for the Vue application. This is where we define global CSS style rules, create/destroy the editor instance, construct/destruct the I/O managers, and construct and provide the state providers. +The entry point for the Svelte application. -## Entry point: `main.ts` +## Editor base instance: `Editor.svelte` -The entry point for the entire project's code bundle. Here we simply initialize the WASM module with `await initWasm();` then initialize the Vue application with `createApp(App).mount("#app");`. +This is where we define global CSS style rules, create/destroy the editor instance, construct/destruct the I/O managers, and construct and `setContext()` the state providers. + +## JS bundle entry point: `main.ts` + +The entry point for the entire project's code bundle. Here we simply initialize the Svelte application with `export default new App({ target: document.body });`. diff --git a/frontend-svelte/src/components/Editor.svelte b/frontend-svelte/src/components/Editor.svelte index b4779f8c..a68f34bf 100644 --- a/frontend-svelte/src/components/Editor.svelte +++ b/frontend-svelte/src/components/Editor.svelte @@ -287,13 +287,14 @@ } // Checkbox needs to apply the focus outline to its sibling label + .optional-input input:focus-visible + label, .checkbox-input input:focus-visible + label { outline: 1px dashed var(--color-e-nearwhite); outline-offset: -1px; + } - // Variant: dark outline over light colors - &.checked { - outline: 1px dashed var(--color-2-mildblack); - } + // Variant: dark outline over light colors (when the checkbox is checked) + :not(.optional-input) > .checkbox-input input:focus-visible + label.checked { + outline: 1px dashed var(--color-2-mildblack); } diff --git a/frontend-svelte/src/components/README.md b/frontend-svelte/src/components/README.md index 94ee392a..24d3b6d1 100644 --- a/frontend-svelte/src/components/README.md +++ b/frontend-svelte/src/components/README.md @@ -1,6 +1,6 @@ # Overview of `/frontend-svelte/src/components/` -Each component represents a (usually reusable) part of the Graphite Editor GUI. These all get mounted within the Vue entry point, `App.svelte`, in the `/src` directory above this one. +Each component represents a (usually reusable) part of the Graphite Editor GUI. These all get mounted in `Editor.svelte` (in the `/src` directory above this one). ## Floating Menus: `floating-menus/` @@ -31,28 +31,30 @@ This section contains a growing list of quick reference information for helpful The component declares this: ```ts -export default defineComponent({ - emits: ["update:theBidirectionalProperty"], - props: { - theBidirectionalProperty: { - type: Number as PropType, - required: false, - }, - }, - watch: { - // Called only when `theBidirectionalProperty` is changed from outside this component (with v-model) - theBidirectionalProperty(newSelectedIndex: number | undefined) {}, - }, - methods: { - doSomething() { - this.$emit("update:theBidirectionalProperty", SOME_NEW_VALUE); - }, - }, -}); +// The dispatcher that sends the changed value as a custom event to the parent +const dispatch = createEventDispatcher<{ theBidirectionalProperty: number }>(); + +// The prop +export let theBidirectionalProperty: number; + +// Called only when `theBidirectionalProperty` is changed from outside this component via its props +$: console.log(theBidirectionalProperty); + +// Example of a method that would update the value +function doSomething() { + dispatch("theBidirectionalProperty", SOME_NEW_VALUE); +}, ``` Users of the component do this for `theCorrespondingDataEntry` to be a two-way binding: -```html - +```ts +let theCorrespondingDataEntry = 42; +``` + +```svelte + { theCorrespondingDataEntry = detail; }} +/> ``` diff --git a/frontend-svelte/src/components/floating-menus/ColorPicker.svelte b/frontend-svelte/src/components/floating-menus/ColorPicker.svelte index d1ae9c8c..cd36f5e2 100644 --- a/frontend-svelte/src/components/floating-menus/ColorPicker.svelte +++ b/frontend-svelte/src/components/floating-menus/ColorPicker.svelte @@ -46,35 +46,42 @@ const hsvaOrNone = color.toHSVA(); const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 }; + // New color components let hue = hsva.h; let saturation = hsva.s; let value = hsva.v; let alpha = hsva.a; let isNone = hsvaOrNone === undefined; + // Initial color components let initialHue = hsva.h; let initialSaturation = hsva.s; let initialValue = hsva.v; let initialAlpha = hsva.a; let initialIsNone = hsvaOrNone === undefined; + // Transient state let draggingPickerTrack: HTMLDivElement | undefined = undefined; - let colorSpaceChoices = COLOR_SPACE_CHOICES; let strayCloses = true; $: rgbChannels = Object.entries(newColor.toRgb255() || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][]; $: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][]; $: opaqueHueColor = new Color({ h: hue, s: 1, v: 1, a: 1 }); $: newColor = isNone ? new Color("none") : new Color({ h: hue, s: saturation, v: value, a: alpha }); - $: initialColor = initialIsNone ? new Color("none") : new Color({ h: initialHue, s: initialSaturation, v: initialValue, a: initialAlpha }); + $: initialColor = updateInitialColor(initialHue, initialSaturation, initialValue, initialAlpha, initialIsNone, open); $: watchOpen(open); $: watchColor(color); - // Called only when `open` is changed from outside this component (with v-model) - function watchOpen(open: boolean) { - if (open) setInitialHSVA(hue, saturation, value, alpha, isNone); + // Taking `_open` is necessary to make Svelte order the reactive processing queue so this works as required, see: + // https://stackoverflow.com/questions/63934543/svelte-reactivity-not-triggering-when-variable-changed-in-a-function + function updateInitialColor(h: number, s: number, v: number, a: number, initialIsNone: boolean, _open: boolean) { + if (initialIsNone) return new Color("none"); + return new Color({ h, s, v, a }); + } + + function watchOpen(open: boolean) { + if (!open) setInitialHSVA(hue, saturation, value, alpha, isNone); } - // Called only when `color` is changed from outside this component (with v-model) function watchColor(color: Color) { const hsva = color.toHSVA(); @@ -222,20 +229,20 @@ setColor(presetColor); } - function setNewHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) { - hue = hue; - saturation = saturation; - value = value; - alpha = alpha; - isNone = isNone; + function setNewHSVA(h: number, s: number, v: number, a: number, none: boolean) { + hue = h; + saturation = s; + value = v; + alpha = a; + isNone = none; } - function setInitialHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) { - initialHue = hue; - initialSaturation = saturation; - initialValue = value; - initialAlpha = alpha; - initialIsNone = isNone; + function setInitialHSVA(h: number, s: number, v: number, a: number, none: boolean) { + initialHue = h; + initialSaturation = s; + initialValue = v; + initialAlpha = a; + initialIsNone = none; } async function activateEyedropperSample() { @@ -297,7 +304,7 @@ Initial - + Hex @@ -333,7 +340,7 @@ - HSV diff --git a/frontend-svelte/src/components/floating-menus/MenuList.svelte b/frontend-svelte/src/components/floating-menus/MenuList.svelte index 68588a25..8a5118c3 100644 --- a/frontend-svelte/src/components/floating-menus/MenuList.svelte +++ b/frontend-svelte/src/components/floating-menus/MenuList.svelte @@ -33,7 +33,7 @@ let highlighted = activeEntry as MenuListEntry | undefined; let virtualScrollingEntriesStart = 0; - // Called only when `open` is changed from outside this component (with v-model) + // Called only when `open` is changed from outside this component $: watchOpen(open); $: watchRemeasureWidth(entries, drawIcon); $: virtualScrollingTotalHeight = entries.length === 0 ? 0 : entries[0].length * virtualScrollingEntryHeight; @@ -58,7 +58,7 @@ // Call the action if available if (menuListEntry.action) menuListEntry.action(); - // Emit the clicked entry as the new active entry + // Notify the parent about the clicked entry as the new active entry dispatch("activeEntry", menuListEntry); // Close the containing menu diff --git a/frontend-svelte/src/components/layout/FloatingMenu.svelte b/frontend-svelte/src/components/layout/FloatingMenu.svelte index 40b23fd1..56bcb356 100644 --- a/frontend-svelte/src/components/layout/FloatingMenu.svelte +++ b/frontend-svelte/src/components/layout/FloatingMenu.svelte @@ -62,7 +62,7 @@ $: watchOpenChange(open); - // Called only when `open` is changed from outside this component (with `v-model`) + // Called only when `open` is changed from outside this component async function watchOpenChange(isOpen: boolean) { // Switching from closed to open if (isOpen && !wasOpen) { @@ -102,9 +102,9 @@ } // Gets the client bounds of the elements and apply relevant styles to them - // TODO: Use the Vue :style attribute more whilst not causing recursive updates + // TODO: Use DOM attribute bindings more whilst not causing recursive updates afterUpdate(() => { - // Turning measuring on and off both causes the component to change, which causes the `updated()` Vue event to fire extraneous times (hurting performance and sometimes causing an infinite loop) + // Turning measuring on and off both causes the component to change, which causes the `afterUpdate()` Svelte event to fire extraneous times (hurting performance and sometimes causing an infinite loop) if (!measuringOngoingGuard) positionAndStyleFloatingMenu(); }); @@ -128,7 +128,7 @@ if (!inParentFloatingMenu) { // 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 + // 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`; @@ -136,7 +136,7 @@ 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 + // 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 (tail && direction === "Bottom") tail.style.top = `${floatingMenuBounds.top}px`; if (tail && direction === "Top") tail.style.bottom = `${floatingMenuBounds.bottom}px`; if (tail && direction === "Right") tail.style.left = `${floatingMenuBounds.left}px`; @@ -150,7 +150,7 @@ if (direction === "Top" || direction === "Bottom") { zeroedBorderVertical = direction === "Top" ? "Bottom" : "Top"; - // 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 + // 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`; if (workspaceBounds.left + floatingMenuContainerBounds.left === 12) zeroedBorderHorizontal = "Left"; @@ -163,7 +163,7 @@ if (direction === "Left" || direction === "Right") { zeroedBorderHorizontal = direction === "Left" ? "Right" : "Left"; - // 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 + // 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`; if (workspaceBounds.top + floatingMenuContainerBounds.top === 12) zeroedBorderVertical = "Top"; @@ -176,7 +176,7 @@ // Remove the rounded corner from the content where the tail perfectly meets the corner if (type === "Popover" && windowEdgeMargin === 6 && zeroedBorderVertical && zeroedBorderHorizontal) { - // 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 + // 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"; @@ -204,7 +204,7 @@ export async function measureAndEmitNaturalWidth(): Promise { if (!measuringOngoingGuard) return; - // Wait for the changed content which fired the `updated()` Vue event to be put into the DOM + // Wait for the changed content which fired the `afterUpdate()` Svelte event to be put into the DOM await tick(); // Wait until all fonts have been loaded and rendered so measurements of content involving text are accurate @@ -216,15 +216,15 @@ await tick(); // 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 + // 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; - // 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 + // 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; await tick(); measuringOngoingGuard = false; - // Emit the measured natural width to the parent + // Notify the parent about the measured natural width if (naturalWidth !== undefined && naturalWidth >= 0) { dispatch("naturalWidth", naturalWidth); } diff --git a/frontend-svelte/src/components/panels/NodeGraph.svelte b/frontend-svelte/src/components/panels/NodeGraph.svelte index e3a7f72b..3139a883 100644 --- a/frontend-svelte/src/components/panels/NodeGraph.svelte +++ b/frontend-svelte/src/components/panels/NodeGraph.svelte @@ -244,8 +244,7 @@ y: Math.round(((e.clientY - graphBounds.y) / transform.scale - transform.y) / GRID_SIZE), }; - // Find actual relevant child and focus it - // TODO: Svelte: check if this works and if `setTimeout` can be removed + // Find actual relevant child and focus it (setTimeout is required to actually focus the input element) setTimeout(() => nodeSearchInput.focus(), 0); document.addEventListener("keydown", keydown); diff --git a/frontend-svelte/src/components/widgets/WidgetRow.svelte b/frontend-svelte/src/components/widgets/WidgetRow.svelte index a6bec62f..3b2f0e3d 100644 --- a/frontend-svelte/src/components/widgets/WidgetRow.svelte +++ b/frontend-svelte/src/components/widgets/WidgetRow.svelte @@ -80,7 +80,7 @@ } - +
diff --git a/frontend-svelte/src/components/widgets/inputs/ColorInput.svelte b/frontend-svelte/src/components/widgets/inputs/ColorInput.svelte index 601b578c..94a89de7 100644 --- a/frontend-svelte/src/components/widgets/inputs/ColorInput.svelte +++ b/frontend-svelte/src/components/widgets/inputs/ColorInput.svelte @@ -35,7 +35,16 @@ {chip} {/if} - (open = detail)} color={value} on:color={({ detail }) => dispatch("value", detail)} allowNone={true} /> + (open = detail)} + color={value} + on:color={({ detail }) => { + value = detail; + dispatch("value", detail); + }} + allowNone={true} + />