Clean up Vue component refs (#813)

* Clean up Vue component refs

* Second pass of code improvements
This commit is contained in:
Keavon Chambers 2022-10-24 20:02:49 -07:00
parent cee1add3a4
commit d2e23d6b15
21 changed files with 253 additions and 195 deletions

View File

@ -135,7 +135,7 @@ export default defineComponent({
const hsva = this.color.toHSVA(); const hsva = this.color.toHSVA();
return { return {
draggingPickerTrack: undefined as HTMLElement | undefined, draggingPickerTrack: undefined as HTMLDivElement | undefined,
hue: hsva.h, hue: hsva.h,
saturation: hsva.s, saturation: hsva.s,
value: hsva.v, value: hsva.v,

View File

@ -84,8 +84,8 @@ export default defineComponent({
}, },
mounted() { mounted() {
// Focus the first button in the popup // Focus the first button in the popup
const element = this.$el as Element | undefined; const dialogModal: HTMLDivElement | undefined = this.$el;
const emphasizedOrFirstButton = (element?.querySelector("[data-emphasized]") || element?.querySelector("[data-text-button]") || undefined) as HTMLButtonElement | undefined; const emphasizedOrFirstButton = (dialogModal?.querySelector("[data-emphasized]") || dialogModal?.querySelector("[data-text-button]") || undefined) as HTMLButtonElement | undefined;
emphasizedOrFirstButton?.focus(); emphasizedOrFirstButton?.focus();
}, },
components: { components: {

View File

@ -113,7 +113,9 @@ export default defineComponent({
}, },
methods: { methods: {
displayImageDataPreview(imageData: ImageData | undefined) { displayImageDataPreview(imageData: ImageData | undefined) {
const canvas = this.$refs.zoomPreviewCanvas as HTMLCanvasElement; const canvas = this.$refs.zoomPreviewCanvas as HTMLCanvasElement | undefined;
if (!canvas) return;
canvas.width = ZOOM_WINDOW_DIMENSIONS; canvas.width = ZOOM_WINDOW_DIMENSIONS;
canvas.height = ZOOM_WINDOW_DIMENSIONS; canvas.height = ZOOM_WINDOW_DIMENSIONS;
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");

View File

@ -46,7 +46,7 @@
:direction="'TopRight'" :direction="'TopRight'"
:entries="entry.children" :entries="entry.children"
v-bind="{ minWidth, drawIcon, scrollableY }" v-bind="{ minWidth, drawIcon, scrollableY }"
:ref="(ref: MenuListInstance) => ref && (entry.ref = ref)" :ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
/> />
</LayoutRow> </LayoutRow>
</template> </template>
@ -204,15 +204,17 @@ const MenuList = defineComponent({
this.$emit("update:open", newIsOpen); this.$emit("update:open", newIsOpen);
}, },
entries() { entries() {
const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu; (this.$refs.floatingMenu as typeof FloatingMenu | undefined)?.measureAndEmitNaturalWidth();
floatingMenu.measureAndEmitNaturalWidth();
}, },
drawIcon() { drawIcon() {
const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu; (this.$refs.floatingMenu as typeof FloatingMenu | undefined)?.measureAndEmitNaturalWidth();
floatingMenu.measureAndEmitNaturalWidth();
}, },
}, },
methods: { methods: {
scrollViewTo(distanceDown: number): void {
const scroller: HTMLDivElement | undefined = (this.$refs.scroller as typeof LayoutCol | undefined)?.$el;
scroller?.scrollTo(0, distanceDown);
},
onEntryClick(menuListEntry: MenuListEntry): void { onEntryClick(menuListEntry: MenuListEntry): void {
// Call the action if available // Call the action if available
if (menuListEntry.action) menuListEntry.action(); if (menuListEntry.action) menuListEntry.action();
@ -242,7 +244,6 @@ const MenuList = defineComponent({
return this.open; return this.open;
}, },
/// Handles keyboard navigation for the menu. Returns if the entire menu stack should be dismissed /// Handles keyboard navigation for the menu. Returns if the entire menu stack should be dismissed
keydown(e: KeyboardEvent, submenu: boolean): boolean { keydown(e: KeyboardEvent, submenu: boolean): boolean {
// Interactive menus should keep the active entry the same as the highlighted one // Interactive menus should keep the active entry the same as the highlighted one

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" ref="floatingMenu"> <div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]">
<div class="tail" v-if="open && type === 'Popover'" ref="tail"></div> <div class="tail" :style="tailStyle" v-if="displayTail"></div>
<div class="floating-menu-container" v-if="open || measuringOngoing" ref="floatingMenuContainer"> <div class="floating-menu-container" v-if="open || measuringOngoing" ref="floatingMenuContainer">
<LayoutCol class="floating-menu-content" :style="{ minWidth: minWidthStyleValue }" :scrollableY="scrollableY" ref="floatingMenuContent" data-floating-menu-content> <LayoutCol class="floating-menu-content" :style="{ minWidth: minWidthStyleValue }" :scrollableY="scrollableY" ref="floatingMenuContent" data-floating-menu-content>
<slot></slot> <slot></slot>
@ -203,6 +203,8 @@ export default defineComponent({
escapeCloses: { type: Boolean as PropType<boolean>, default: true }, escapeCloses: { type: Boolean as PropType<boolean>, default: true },
}, },
data() { data() {
const tailStyle: { top?: string; bottom?: string; left?: string; right?: string } = {};
// 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. // 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 // 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
// tell the floating menu content to use it as a min-width so the floating menu is at least the width of the parent element's floating menu spawner. // tell the floating menu content to use it as a min-width so the floating menu is at least the width of the parent element's floating menu spawner.
@ -216,6 +218,7 @@ export default defineComponent({
measuringOngoing: false, measuringOngoing: false,
measuringOngoingGuard: false, measuringOngoingGuard: false,
minWidthParentWidth: 0, minWidthParentWidth: 0,
tailStyle,
containerResizeObserver, containerResizeObserver,
pointerStillDown: false, pointerStillDown: false,
workspaceBounds: new DOMRect(), workspaceBounds: new DOMRect(),
@ -228,6 +231,9 @@ export default defineComponent({
if (this.measuringOngoing) return "0"; if (this.measuringOngoing) return "0";
return `${Math.max(this.minWidth, this.minWidthParentWidth)}px`; return `${Math.max(this.minWidth, this.minWidthParentWidth)}px`;
}, },
displayTail() {
return this.open && this.type === "Popover";
},
}, },
// Gets the client bounds of the elements and apply relevant styles to them // 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 the Vue :style attribute more whilst not causing recursive updates
@ -245,15 +251,15 @@ export default defineComponent({
if (this.type === "Cursor") return; if (this.type === "Cursor") return;
const workspace = document.querySelector("[data-workspace]"); const workspace = document.querySelector("[data-workspace]");
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement; const floatingMenu: HTMLDivElement | undefined = this.$el;
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol; const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLDivElement | undefined;
const floatingMenuContent: HTMLElement | undefined = floatingMenuContentComponent?.$el; const floatingMenuContent: HTMLDivElement | undefined = (this.$refs.floatingMenuContent as typeof LayoutCol | undefined)?.$el;
const floatingMenu = this.$refs.floatingMenu as HTMLElement;
if (!workspace || !floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !floatingMenu) return; if (!workspace || !floatingMenu || !floatingMenuContainer || !floatingMenuContent) return;
this.workspaceBounds = workspace.getBoundingClientRect(); this.workspaceBounds = workspace.getBoundingClientRect();
this.floatingMenuBounds = floatingMenu.getBoundingClientRect(); this.floatingMenuBounds = floatingMenu.getBoundingClientRect();
const floatingMenuContainerBounds = floatingMenuContainer.getBoundingClientRect();
this.floatingMenuContentBounds = floatingMenuContent.getBoundingClientRect(); this.floatingMenuContentBounds = floatingMenuContent.getBoundingClientRect();
const inParentFloatingMenu = Boolean(floatingMenuContainer.closest("[data-floating-menu-content]")); const inParentFloatingMenu = Boolean(floatingMenuContainer.closest("[data-floating-menu-content]"));
@ -267,13 +273,10 @@ export default defineComponent({
if (this.direction === "Left") floatingMenuContent.style.right = `${tailOffset + this.floatingMenuBounds.right}px`; if (this.direction === "Left") floatingMenuContent.style.right = `${tailOffset + this.floatingMenuBounds.right}px`;
// Required to correctly position tail when scrolled (it has a `position: fixed` to prevent clipping) // Required to correctly position tail when scrolled (it has a `position: fixed` to prevent clipping)
const tail = this.$refs.tail as HTMLElement; if (this.direction === "Bottom") this.tailStyle = { top: `${this.floatingMenuBounds.top}px` };
if (tail) { if (this.direction === "Top") this.tailStyle = { bottom: `${this.floatingMenuBounds.bottom}px` };
if (this.direction === "Bottom") tail.style.top = `${this.floatingMenuBounds.top}px`; if (this.direction === "Right") this.tailStyle = { left: `${this.floatingMenuBounds.left}px` };
if (this.direction === "Top") tail.style.bottom = `${this.floatingMenuBounds.bottom}px`; if (this.direction === "Left") this.tailStyle = { right: `${this.floatingMenuBounds.right}px` };
if (this.direction === "Right") tail.style.left = `${this.floatingMenuBounds.left}px`;
if (this.direction === "Left") tail.style.right = `${this.floatingMenuBounds.right}px`;
}
} }
type Edge = "Top" | "Bottom" | "Left" | "Right"; type Edge = "Top" | "Bottom" | "Left" | "Right";
@ -285,11 +288,11 @@ export default defineComponent({
if (this.floatingMenuContentBounds.left - this.windowEdgeMargin <= this.workspaceBounds.left) { if (this.floatingMenuContentBounds.left - this.windowEdgeMargin <= this.workspaceBounds.left) {
floatingMenuContent.style.left = `${this.windowEdgeMargin}px`; floatingMenuContent.style.left = `${this.windowEdgeMargin}px`;
if (this.workspaceBounds.left + floatingMenuContainer.getBoundingClientRect().left === 12) zeroedBorderHorizontal = "Left"; if (this.workspaceBounds.left + floatingMenuContainerBounds.left === 12) zeroedBorderHorizontal = "Left";
} }
if (this.floatingMenuContentBounds.right + this.windowEdgeMargin >= this.workspaceBounds.right) { if (this.floatingMenuContentBounds.right + this.windowEdgeMargin >= this.workspaceBounds.right) {
floatingMenuContent.style.right = `${this.windowEdgeMargin}px`; floatingMenuContent.style.right = `${this.windowEdgeMargin}px`;
if (this.workspaceBounds.right - floatingMenuContainer.getBoundingClientRect().right === 12) zeroedBorderHorizontal = "Right"; if (this.workspaceBounds.right - floatingMenuContainerBounds.right === 12) zeroedBorderHorizontal = "Right";
} }
} }
if (this.direction === "Left" || this.direction === "Right") { if (this.direction === "Left" || this.direction === "Right") {
@ -297,11 +300,11 @@ export default defineComponent({
if (this.floatingMenuContentBounds.top - this.windowEdgeMargin <= this.workspaceBounds.top) { if (this.floatingMenuContentBounds.top - this.windowEdgeMargin <= this.workspaceBounds.top) {
floatingMenuContent.style.top = `${this.windowEdgeMargin}px`; floatingMenuContent.style.top = `${this.windowEdgeMargin}px`;
if (this.workspaceBounds.top + floatingMenuContainer.getBoundingClientRect().top === 12) zeroedBorderVertical = "Top"; if (this.workspaceBounds.top + floatingMenuContainerBounds.top === 12) zeroedBorderVertical = "Top";
} }
if (this.floatingMenuContentBounds.bottom + this.windowEdgeMargin >= this.workspaceBounds.bottom) { if (this.floatingMenuContentBounds.bottom + this.windowEdgeMargin >= this.workspaceBounds.bottom) {
floatingMenuContent.style.bottom = `${this.windowEdgeMargin}px`; floatingMenuContent.style.bottom = `${this.windowEdgeMargin}px`;
if (this.workspaceBounds.bottom - floatingMenuContainer.getBoundingClientRect().bottom === 12) zeroedBorderVertical = "Bottom"; if (this.workspaceBounds.bottom - floatingMenuContainerBounds.bottom === 12) zeroedBorderVertical = "Bottom";
} }
} }
@ -338,16 +341,12 @@ export default defineComponent({
// Make the component show itself with 0 min-width so it can be measured, and wait until the values have been updated to the DOM // Make the component show itself with 0 min-width so it can be measured, and wait until the values have been updated to the DOM
this.measuringOngoing = true; this.measuringOngoing = true;
this.measuringOngoingGuard = true; this.measuringOngoingGuard = true;
await nextTick(); await nextTick();
// Only measure if the menu is visible, perhaps because a parent component with a `v-if` condition is false // Measure the width of the floating menu content element, if it's currently visible
let naturalWidth; // The result will be `undefined` if the menu is invisible, perhaps because an ancestor component is hidden with a falsy `v-if` condition
if (this.$refs.floatingMenuContent) { const floatingMenuContent: HTMLDivElement | undefined = (this.$refs.floatingMenuContent as typeof LayoutCol | undefined)?.$el;
// Measure the width of the floating menu content element const naturalWidth = floatingMenuContent?.clientWidth;
const floatingMenuContent: HTMLElement = (this.$refs.floatingMenuContent as typeof LayoutCol).$el;
naturalWidth = floatingMenuContent?.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 `updated()` Vue event, so we can turn off the protection after that has happened
this.measuringOngoing = false; this.measuringOngoing = false;
@ -363,7 +362,8 @@ export default defineComponent({
const target = e.target as HTMLElement | undefined; const target = e.target as HTMLElement | undefined;
const pointerOverFloatingMenuKeepOpen = target?.closest("[data-hover-menu-keep-open]") as HTMLElement | undefined; const pointerOverFloatingMenuKeepOpen = target?.closest("[data-hover-menu-keep-open]") as HTMLElement | undefined;
const pointerOverFloatingMenuSpawner = target?.closest("[data-hover-menu-spawner]") as HTMLElement | undefined; const pointerOverFloatingMenuSpawner = target?.closest("[data-hover-menu-spawner]") as HTMLElement | undefined;
const pointerOverOwnFloatingMenuSpawner = pointerOverFloatingMenuSpawner?.parentElement?.contains(this.$refs.floatingMenu as HTMLElement); const floatingMenu: HTMLDivElement | undefined = this.$el;
const pointerOverOwnFloatingMenuSpawner = floatingMenu && pointerOverFloatingMenuSpawner?.parentElement?.contains(floatingMenu);
// Swap this open floating menu with the one created by the floating menu spawner being hovered over // Swap this open floating menu with the one created by the floating menu spawner being hovered over
if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) { if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) {
@ -418,10 +418,13 @@ export default defineComponent({
}, },
isPointerEventOutsideFloatingMenu(e: PointerEvent, extraDistanceAllowed = 0): boolean { isPointerEventOutsideFloatingMenu(e: PointerEvent, extraDistanceAllowed = 0): boolean {
// Considers all child menus as well as the top-level one. // Considers all child menus as well as the top-level one.
const allContainedFloatingMenus = [...this.$el.querySelectorAll("[data-floating-menu-content]")]; const floatingMenu: HTMLDivElement | undefined = this.$el;
if (!floatingMenu) return true;
const allContainedFloatingMenus = [...floatingMenu.querySelectorAll("[data-floating-menu-content]")];
return !allContainedFloatingMenus.find((element) => !this.isPointerEventOutsideMenuElement(e, element, extraDistanceAllowed)); return !allContainedFloatingMenus.find((element) => !this.isPointerEventOutsideMenuElement(e, element, extraDistanceAllowed));
}, },
isPointerEventOutsideMenuElement(e: PointerEvent, element: HTMLElement, extraDistanceAllowed = 0): boolean { isPointerEventOutsideMenuElement(e: PointerEvent, element: Element, extraDistanceAllowed = 0): boolean {
const floatingMenuBounds = element.getBoundingClientRect(); const floatingMenuBounds = element.getBoundingClientRect();
if (floatingMenuBounds.left - e.clientX >= extraDistanceAllowed) return true; if (floatingMenuBounds.left - e.clientX >= extraDistanceAllowed) return true;
@ -450,7 +453,7 @@ export default defineComponent({
await nextTick(); await nextTick();
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement; const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLDivElement | undefined;
if (!floatingMenuContainer) return; if (!floatingMenuContainer) return;
// Start a new observation of the now-open floating menu // Start a new observation of the now-open floating menu

View File

@ -37,7 +37,7 @@
:imageData="cursorEyedropperPreviewImageData" :imageData="cursorEyedropperPreviewImageData"
:style="{ left: cursorLeft + 'px', top: cursorTop + 'px' }" :style="{ left: cursorLeft + 'px', top: cursorTop + 'px' }"
/> />
<div class="canvas" @pointerdown="(e: PointerEvent) => canvasPointerDown(e)" @dragover="(e) => e.preventDefault()" @drop="(e) => pasteFile(e)" ref="canvas" data-canvas> <div class="canvas" @pointerdown="(e: PointerEvent) => canvasPointerDown(e)" @dragover="(e) => e.preventDefault()" @drop="(e) => pasteFile(e)" ref="canvasDiv" data-canvas>
<svg class="artboards" v-html="artboardSvg" :style="{ width: canvasWidthCSS, height: canvasHeightCSS }"></svg> <svg class="artboards" v-html="artboardSvg" :style="{ width: canvasWidthCSS, height: canvasHeightCSS }"></svg>
<svg <svg
class="artwork" class="artwork"
@ -341,10 +341,8 @@ export default defineComponent({
}, },
canvasPointerDown(e: PointerEvent) { canvasPointerDown(e: PointerEvent) {
const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable; const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable;
if (!onEditbox) {
const canvas = this.$refs.canvas as HTMLElement; if (!onEditbox) (this.$refs.canvasDiv as HTMLDivElement | undefined)?.setPointerCapture(e.pointerId);
canvas.setPointerCapture(e.pointerId);
}
}, },
// Update rendered SVGs // Update rendered SVGs
async updateDocumentArtwork(svg: string) { async updateDocumentArtwork(svg: string) {
@ -354,8 +352,10 @@ export default defineComponent({
await nextTick(); await nextTick();
if (this.textInput) { if (this.textInput) {
const canvas = this.$refs.canvas as HTMLElement; const canvasDiv = this.$refs.canvasDiv as HTMLDivElement | undefined;
const foreignObject = canvas.getElementsByTagName("foreignObject")[0] as SVGForeignObjectElement; if (!canvasDiv) return;
const foreignObject = canvasDiv.getElementsByTagName("foreignObject")[0] as SVGForeignObjectElement;
if (foreignObject.children.length > 0) return; if (foreignObject.children.length > 0) return;
const addedInput = foreignObject.appendChild(this.textInput); const addedInput = foreignObject.appendChild(this.textInput);
@ -496,17 +496,15 @@ export default defineComponent({
// Resize elements to render the new viewport size // Resize elements to render the new viewport size
viewportResize() { viewportResize() {
// Resize the canvas // Resize the canvas
const canvas = this.$refs.canvas as HTMLElement; const canvasDiv = this.$refs.canvasDiv as HTMLDivElement | undefined;
const width = Math.ceil(parseFloat(getComputedStyle(canvas).width)); if (!canvasDiv) return;
const height = Math.ceil(parseFloat(getComputedStyle(canvas).height));
this.canvasSvgWidth = width; this.canvasSvgWidth = Math.ceil(parseFloat(getComputedStyle(canvasDiv).width));
this.canvasSvgHeight = height; this.canvasSvgHeight = Math.ceil(parseFloat(getComputedStyle(canvasDiv).height));
// Resize the rulers // Resize the rulers
const rulerHorizontal = this.$refs.rulerHorizontal as typeof CanvasRuler; (this.$refs.rulerHorizontal as typeof CanvasRuler | undefined)?.resize();
const rulerVertical = this.$refs.rulerVertical as typeof CanvasRuler; (this.$refs.rulerVertical as typeof CanvasRuler | undefined)?.resize();
rulerHorizontal?.resize();
rulerVertical?.resize();
}, },
canvasDimensionCSS(dimension: number | undefined): string { canvasDimensionCSS(dimension: number | undefined): string {
// Temporary placeholder until the first actual value is populated // Temporary placeholder until the first actual value is populated

View File

@ -4,7 +4,7 @@
<WidgetLayout :layout="layerTreeOptionsLayout" /> <WidgetLayout :layout="layerTreeOptionsLayout" />
</LayoutRow> </LayoutRow>
<LayoutRow class="layer-tree-rows" :scrollableY="true"> <LayoutRow class="layer-tree-rows" :scrollableY="true">
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="(e: DragEvent) => draggable && updateInsertLine(e)" @dragend="() => draggable && drop()"> <LayoutCol class="list" ref="list" @click="() => deselectAllLayers()" @dragover="(e: DragEvent) => draggable && updateInsertLine(e)" @dragend="() => draggable && drop()">
<LayoutRow <LayoutRow
class="layer-row" class="layer-row"
v-for="(listing, index) in layers" v-for="(listing, index) in layers"
@ -13,7 +13,7 @@
> >
<LayoutRow class="visibility"> <LayoutRow class="visibility">
<IconButton <IconButton
:action="(e: MouseEvent) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())" :action="(e?: MouseEvent) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())"
:size="24" :size="24"
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'" :icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
:title="listing.entry.visible ? 'Visible' : 'Hidden'" :title="listing.entry.visible ? 'Visible' : 'Hidden'"
@ -56,8 +56,8 @@
:disabled="!listing.editingName" :disabled="!listing.editingName"
@blur="() => onEditLayerNameDeselect(listing)" @blur="() => onEditLayerNameDeselect(listing)"
@keydown.esc="onEditLayerNameDeselect(listing)" @keydown.esc="onEditLayerNameDeselect(listing)"
@keydown.enter="(e) => onEditLayerNameChange(listing, e.target || undefined)" @keydown.enter="(e) => onEditLayerNameChange(listing, e)"
@change="(e) => onEditLayerNameChange(listing, e.target || undefined)" @change="(e) => onEditLayerNameChange(listing, e)"
/> />
</LayoutRow> </LayoutRow>
<div class="thumbnail" v-html="listing.entry.thumbnail"></div> <div class="thumbnail" v-html="listing.entry.thumbnail"></div>
@ -326,23 +326,24 @@ export default defineComponent({
async onEditLayerName(listing: LayerListingInfo) { async onEditLayerName(listing: LayerListingInfo) {
if (listing.editingName) return; if (listing.editingName) return;
listing.editingName = true;
this.draggable = false; this.draggable = false;
listing.editingName = true;
const tree: HTMLElement = (this.$refs.layerTreeList as typeof LayoutCol).$el;
await nextTick(); await nextTick();
(tree.querySelector("[data-text-input]:not([disabled])") as HTMLInputElement).select();
const tree: HTMLDivElement | undefined = (this.$refs.list as typeof LayoutCol | undefined)?.$el;
const textInput: HTMLInputElement | undefined = tree?.querySelector("[data-text-input]:not([disabled])") || undefined;
textInput?.select();
}, },
onEditLayerNameChange(listing: LayerListingInfo, inputElement: EventTarget | undefined) { onEditLayerNameChange(listing: LayerListingInfo, e: Event) {
// Eliminate duplicate events // Eliminate duplicate events
if (!listing.editingName) return; if (!listing.editingName) return;
this.draggable = true; this.draggable = true;
const name = (inputElement as HTMLInputElement).value; const name = (e.target as HTMLInputElement | undefined)?.value;
listing.editingName = false; listing.editingName = false;
this.editor.instance.setLayerName(listing.entry.path, name); if (name) this.editor.instance.setLayerName(listing.entry.path, name);
}, },
async onEditLayerNameDeselect(listing: LayerListingInfo) { async onEditLayerNameDeselect(listing: LayerListingInfo) {
this.draggable = true; this.draggable = true;
@ -368,7 +369,7 @@ export default defineComponent({
async deselectAllLayers() { async deselectAllLayers() {
this.editor.instance.deselectAllLayers(); this.editor.instance.deselectAllLayers();
}, },
calculateDragIndex(tree: HTMLElement, clientY: number): DraggingData { calculateDragIndex(tree: HTMLDivElement, clientY: number): DraggingData {
const treeChildren = tree.children; const treeChildren = tree.children;
const treeOffset = tree.getBoundingClientRect().top; const treeOffset = tree.getBoundingClientRect().top;
@ -438,16 +439,16 @@ export default defineComponent({
event.dataTransfer.dropEffect = "move"; event.dataTransfer.dropEffect = "move";
event.dataTransfer.effectAllowed = "move"; event.dataTransfer.effectAllowed = "move";
} }
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el;
this.draggingData = this.calculateDragIndex(tree, event.clientY); const tree: HTMLDivElement | undefined = (this.$refs.list as typeof LayoutCol | undefined)?.$el;
if (tree) this.draggingData = this.calculateDragIndex(tree, event.clientY);
}, },
updateInsertLine(event: DragEvent) { updateInsertLine(event: DragEvent) {
// Stop the drag from being shown as cancelled // Stop the drag from being shown as cancelled
event.preventDefault(); event.preventDefault();
const tree: HTMLElement = (this.$refs.layerTreeList as typeof LayoutCol).$el; const tree: HTMLDivElement | undefined = (this.$refs.list as typeof LayoutCol | undefined)?.$el;
this.draggingData = this.calculateDragIndex(tree, event.clientY); if (tree) this.draggingData = this.calculateDragIndex(tree, event.clientY);
}, },
async drop() { async drop() {
if (this.draggingData) { if (this.draggingData) {

View File

@ -21,10 +21,10 @@
<div class="node" style="--offset-left: 3; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)"> <div class="node" style="--offset-left: 3; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary"> <div class="primary">
<div class="ports"> <div class="ports">
<!-- <div class="input port" data-datatype="raster"> <!-- <div class="input port" data-port="input" data-datatype="raster">
<div></div> <div></div>
</div> --> </div> -->
<div class="output port" data-datatype="raster"> <div class="output port" data-port="output" data-datatype="raster">
<div></div> <div></div>
</div> </div>
</div> </div>
@ -35,10 +35,10 @@
<div class="node" style="--offset-left: 9; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)"> <div class="node" style="--offset-left: 9; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary"> <div class="primary">
<div class="ports"> <div class="ports">
<div class="input port" data-datatype="raster"> <div class="input port" data-port="input" data-datatype="raster">
<div></div> <div></div>
</div> </div>
<div class="output port" data-datatype="raster"> <div class="output port" data-port="output" data-datatype="raster">
<div></div> <div></div>
</div> </div>
</div> </div>
@ -48,10 +48,10 @@
<div class="arguments"> <div class="arguments">
<div class="argument"> <div class="argument">
<div class="ports"> <div class="ports">
<div class="input port" data-datatype="raster" style="--data-color: var(--color-data-raster); --data-color-dim: var(--color-data-vector-dim)"> <div class="input port" data-port="input" data-datatype="raster" style="--data-color: var(--color-data-raster); --data-color-dim: var(--color-data-vector-dim)">
<div></div> <div></div>
</div> </div>
<!-- <div class="output port" data-datatype="raster"> <!-- <div class="output port" data-port="output" data-datatype="raster">
<div></div> <div></div>
</div> --> </div> -->
</div> </div>
@ -62,10 +62,10 @@
<div class="node" style="--offset-left: 15; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)"> <div class="node" style="--offset-left: 15; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary"> <div class="primary">
<div class="ports"> <div class="ports">
<!-- <div class="input port" data-datatype="raster"> <!-- <div class="input port" data-port="input" data-datatype="raster">
<div></div> <div></div>
</div> --> </div> -->
<div class="output port" data-datatype="raster"> <div class="output port" data-port="output" data-datatype="raster">
<div></div> <div></div>
</div> </div>
</div> </div>
@ -76,10 +76,10 @@
<div class="node" style="--offset-left: 21; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)"> <div class="node" style="--offset-left: 21; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary"> <div class="primary">
<div class="ports"> <div class="ports">
<div class="input port" data-datatype="raster"> <div class="input port" data-port="input" data-datatype="raster">
<div></div> <div></div>
</div> </div>
<div class="output port" data-datatype="raster"> <div class="output port" data-port="output" data-datatype="raster">
<div></div> <div></div>
</div> </div>
</div> </div>
@ -89,10 +89,10 @@
<div class="arguments"> <div class="arguments">
<div class="argument"> <div class="argument">
<div class="ports"> <div class="ports">
<div class="input port" data-datatype="raster"> <div class="input port" data-port="input" data-datatype="raster">
<div></div> <div></div>
</div> </div>
<!-- <div class="output port" data-datatype="raster"> <!-- <div class="output port" data-port="output" data-datatype="raster">
<div></div> <div></div>
</div> --> </div> -->
</div> </div>
@ -103,10 +103,10 @@
<div class="node" style="--offset-left: 2; --offset-top: 5; --data-color: var(--color-data-vector); --data-color-dim: var(--color-data-vector-dim)"> <div class="node" style="--offset-left: 2; --offset-top: 5; --data-color: var(--color-data-vector); --data-color-dim: var(--color-data-vector-dim)">
<div class="primary"> <div class="primary">
<div class="ports"> <div class="ports">
<!-- <div class="input port" data-datatype="vector"> <!-- <div class="input port" data-port="input" data-datatype="vector">
<div></div> <div></div>
</div> --> </div> -->
<div class="output port" data-datatype="vector"> <div class="output port" data-port="output" data-datatype="vector">
<div></div> <div></div>
</div> </div>
</div> </div>
@ -117,10 +117,10 @@
<div class="node" style="--offset-left: 6; --offset-top: 7; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)"> <div class="node" style="--offset-left: 6; --offset-top: 7; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary"> <div class="primary">
<div class="ports"> <div class="ports">
<!-- <div class="input port" data-datatype="raster"> <!-- <div class="input port" data-port="input" data-datatype="raster">
<div></div> <div></div>
</div> --> </div> -->
<div class="output port" data-datatype="raster"> <div class="output port" data-port="output" data-datatype="raster">
<div></div> <div></div>
</div> </div>
</div> </div>
@ -131,10 +131,10 @@
<div class="node" style="--offset-left: 12; --offset-top: 7; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)"> <div class="node" style="--offset-left: 12; --offset-top: 7; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary"> <div class="primary">
<div class="ports"> <div class="ports">
<!-- <div class="input port" data-datatype="raster"> <!-- <div class="input port" data-port="input" data-datatype="raster">
<div></div> <div></div>
</div> --> </div> -->
<div class="output port" data-datatype="raster"> <div class="output port" data-port="output" data-datatype="raster">
<div></div> <div></div>
</div> </div>
</div> </div>
@ -145,10 +145,10 @@
<div class="node" style="--offset-left: 12; --offset-top: 9; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)"> <div class="node" style="--offset-left: 12; --offset-top: 9; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary"> <div class="primary">
<div class="ports"> <div class="ports">
<!-- <div class="input port" data-datatype="raster"> <!-- <div class="input port" data-port="input" data-datatype="raster">
<div></div> <div></div>
</div> --> </div> -->
<div class="output port" data-datatype="raster"> <div class="output port" data-port="output" data-datatype="raster">
<div></div> <div></div>
</div> </div>
</div> </div>
@ -355,7 +355,7 @@ export default defineComponent({
return { return {
transform: { scale: 1, x: 0, y: 0 }, transform: { scale: 1, x: 0, y: 0 },
panning: false, panning: false,
drawing: undefined as { port: HTMLElement; output: boolean; path: SVGElement } | undefined, drawing: undefined as { port: HTMLDivElement; output: boolean; path: SVGElement } | undefined,
}; };
}, },
computed: { computed: {
@ -375,7 +375,8 @@ export default defineComponent({
}, },
methods: { methods: {
buildWirePathString(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): string { buildWirePathString(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): string {
const containerBounds = (this.$refs.nodesContainer as HTMLElement).getBoundingClientRect(); const containerBounds = (this.$refs.nodesContainer as HTMLDivElement | undefined)?.getBoundingClientRect();
if (!containerBounds) return "[error]";
const outX = verticalOut ? outputBounds.x + outputBounds.width / 2 : outputBounds.x + outputBounds.width - 1; const outX = verticalOut ? outputBounds.x + outputBounds.width / 2 : outputBounds.x + outputBounds.width - 1;
const outY = verticalOut ? outputBounds.y + 1 : outputBounds.y + outputBounds.height / 2; const outY = verticalOut ? outputBounds.y + 1 : outputBounds.y + outputBounds.height / 2;
@ -402,14 +403,14 @@ export default defineComponent({
verticalIn ? inConnectorX : inConnectorX - horizontalCurve verticalIn ? inConnectorX : inConnectorX - horizontalCurve
},${verticalIn ? inConnectorY + verticalCurve : inConnectorY} ${inConnectorX},${inConnectorY}`; },${verticalIn ? inConnectorY + verticalCurve : inConnectorY} ${inConnectorX},${inConnectorY}`;
}, },
createWirePath(outputPort: HTMLElement, inputPort: HTMLElement, verticalOut: boolean, verticalIn: boolean): SVGPathElement { createWirePath(outputPort: HTMLDivElement, inputPort: HTMLDivElement, verticalOut: boolean, verticalIn: boolean): SVGPathElement {
const pathString = this.buildWirePathString(outputPort.getBoundingClientRect(), inputPort.getBoundingClientRect(), verticalOut, verticalIn); const pathString = this.buildWirePathString(outputPort.getBoundingClientRect(), inputPort.getBoundingClientRect(), verticalOut, verticalIn);
const dataType = outputPort.dataset.datatype; const dataType = outputPort.dataset.datatype;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", pathString); path.setAttribute("d", pathString);
path.setAttribute("style", `--data-color: var(--color-data-${dataType}); --data-color-dim: var(--color-data-${dataType}-dim)`); path.setAttribute("style", `--data-color: var(--color-data-${dataType}); --data-color-dim: var(--color-data-${dataType}-dim)`);
(this.$refs.wiresContainer as HTMLElement).appendChild(path); (this.$refs.wiresContainer as SVGSVGElement | undefined)?.appendChild(path);
return path; return path;
}, },
@ -418,7 +419,9 @@ export default defineComponent({
let zoomFactor = 1 + Math.abs(scroll) * WHEEL_RATE; let zoomFactor = 1 + Math.abs(scroll) * WHEEL_RATE;
if (scroll > 0) zoomFactor = 1 / zoomFactor; if (scroll > 0) zoomFactor = 1 / zoomFactor;
const { x, y, width, height } = ((this.$refs.graph as typeof LayoutCol).$el as HTMLElement).getBoundingClientRect(); const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
if (!graphDiv) return;
const { x, y, width, height } = graphDiv.getBoundingClientRect();
this.transform.scale *= zoomFactor; this.transform.scale *= zoomFactor;
@ -435,7 +438,7 @@ export default defineComponent({
this.transform.y -= (deltaY / this.transform.scale) * zoomFactor; this.transform.y -= (deltaY / this.transform.scale) * zoomFactor;
}, },
pointerDown(e: PointerEvent) { pointerDown(e: PointerEvent) {
const port = (e.target as HTMLElement).closest(".port") as HTMLElement; const port = (e.target as HTMLDivElement).closest("[data-port]") as HTMLDivElement;
if (port) { if (port) {
const output = port.classList.contains("output"); const output = port.classList.contains("output");
@ -444,7 +447,9 @@ export default defineComponent({
} else { } else {
this.panning = true; this.panning = true;
} }
((this.$refs.graph as typeof LayoutCol).$el as HTMLElement).setPointerCapture(e.pointerId);
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
graphDiv?.setPointerCapture(e.pointerId);
}, },
pointerMove(e: PointerEvent) { pointerMove(e: PointerEvent) {
if (this.panning) { if (this.panning) {
@ -461,19 +466,20 @@ export default defineComponent({
} }
}, },
pointerUp(e: PointerEvent) { pointerUp(e: PointerEvent) {
((this.$refs.graph as typeof LayoutCol).$el as HTMLElement).releasePointerCapture(e.pointerId); const graph: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
graph?.releasePointerCapture(e.pointerId);
this.panning = false; this.panning = false;
this.drawing = undefined; this.drawing = undefined;
}, },
}, },
mounted() { mounted() {
const outputPort1 = document.querySelectorAll(".output.port")[4] as HTMLElement; const outputPort1 = document.querySelectorAll(`[data-port="${"output"}"]`)[4] as HTMLDivElement | undefined;
const inputPort1 = document.querySelectorAll(".input.port")[1] as HTMLElement; const inputPort1 = document.querySelectorAll(`[data-port="${"input"}"]`)[1] as HTMLDivElement | undefined;
this.createWirePath(outputPort1, inputPort1, true, true); if (outputPort1 && inputPort1) this.createWirePath(outputPort1, inputPort1, true, true);
const outputPort2 = document.querySelectorAll(".output.port")[6] as HTMLElement; const outputPort2 = document.querySelectorAll(`[data-port="${"output"}"]`)[6] as HTMLDivElement | undefined;
const inputPort2 = document.querySelectorAll(".input.port")[3] as HTMLElement; const inputPort2 = document.querySelectorAll(`[data-port="${"input"}"]`)[3] as HTMLDivElement | undefined;
this.createWirePath(outputPort2, inputPort2, true, false); if (outputPort2 && inputPort2) this.createWirePath(outputPort2, inputPort2, true, false);
}, },
components: { components: {
IconLabel, IconLabel,

View File

@ -1,7 +1,7 @@
<template> <template>
<LayoutRow class="checkbox-input"> <LayoutRow class="checkbox-input">
<input type="checkbox" :id="`checkbox-input-${id}`" :checked="checked" @change="(e) => $emit('update:checked', (e.target as HTMLInputElement).checked)" /> <input type="checkbox" :id="`checkbox-input-${id}`" :checked="checked" @change="(e) => $emit('update:checked', (e.target as HTMLInputElement).checked)" />
<label :for="`checkbox-input-${id}`" tabindex="0" @keydown.enter="(e) => ((e.target as HTMLElement).previousSibling as HTMLInputElement).click()" :title="tooltip"> <label :for="`checkbox-input-${id}`" tabindex="0" @keydown.enter="(e) => toggleCheckboxFromLabel(e)" :title="tooltip">
<LayoutRow class="checkbox-box"> <LayoutRow class="checkbox-box">
<IconLabel :icon="icon" /> <IconLabel :icon="icon" />
</LayoutRow> </LayoutRow>
@ -80,6 +80,11 @@ export default defineComponent({
isChecked() { isChecked() {
return this.checked; return this.checked;
}, },
toggleCheckboxFromLabel(e: KeyboardEvent) {
const target = (e.target || undefined) as HTMLLabelElement | undefined;
const previousSibling = (target?.previousSibling || undefined) as HTMLInputElement | undefined;
previousSibling?.click();
},
}, },
components: { components: {
IconLabel, IconLabel,

View File

@ -6,9 +6,8 @@
:style="{ minWidth: `${minWidth}px` }" :style="{ minWidth: `${minWidth}px` }"
:title="tooltip" :title="tooltip"
@click="() => !disabled && (open = true)" @click="() => !disabled && (open = true)"
@blur="(e: FocusEvent) => blur(e)" @blur="(e: FocusEvent) => unFocusDropdownBox(e)"
@keydown="(e: KeyboardEvent) => keydown(e)" @keydown="(e: KeyboardEvent) => keydown(e)"
ref="dropdownBox"
tabindex="0" tabindex="0"
data-hover-menu-spawner data-hover-menu-spawner
> >
@ -155,10 +154,12 @@ export default defineComponent({
return DASH_ENTRY; return DASH_ENTRY;
}, },
keydown(e: KeyboardEvent) { keydown(e: KeyboardEvent) {
(this.$refs.menuList as typeof MenuList).keydown(e, false); (this.$refs.menuList as typeof MenuList | undefined)?.keydown(e, false);
}, },
blur(e: FocusEvent) { unFocusDropdownBox(e: FocusEvent) {
if ((e.target as HTMLElement).closest("[data-dropdown-input]") !== this.$el) this.open = false; const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]");
const self: HTMLDivElement | undefined = this.$el;
if (blurTarget !== self) this.open = false;
}, },
}, },
components: { components: {

View File

@ -140,6 +140,28 @@ export default defineComponent({
macKeyboardLayout: platformIsMac(), macKeyboardLayout: platformIsMac(),
}; };
}, },
methods: {
// Select (highlight) all the text. For technical reasons, it is necessary to pass the current text.
selectAllText(currentText: string) {
const inputElement = this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined;
if (!inputElement) return;
// Setting the value directly is required to make `inputElement.select()` work
inputElement.value = currentText;
inputElement.select();
},
unFocus() {
(this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined)?.blur();
},
getInputElementValue(): string | undefined {
return (this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined)?.value;
},
setInputElementValue(value: string) {
const inputElement = this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined;
if (inputElement) inputElement.value = value;
},
},
computed: { computed: {
inputValue: { inputValue: {
get() { get() {

View File

@ -5,7 +5,6 @@
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" /> <IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
</LayoutRow> </LayoutRow>
<MenuList <MenuList
ref="menulist"
v-model:activeEntry="activeEntry" v-model:activeEntry="activeEntry"
v-model:open="open" v-model:open="open"
:entries="[entries]" :entries="[entries]"
@ -13,6 +12,7 @@
:virtualScrollingEntryHeight="isStyle ? 0 : 20" :virtualScrollingEntryHeight="isStyle ? 0 : 20"
:scrollableY="true" :scrollableY="true"
@naturalWidth="(newNaturalWidth: number) => (isStyle && (minWidth = newNaturalWidth))" @naturalWidth="(newNaturalWidth: number) => (isStyle && (minWidth = newNaturalWidth))"
ref="menuList"
></MenuList> ></MenuList>
</LayoutRow> </LayoutRow>
</template> </template>
@ -74,8 +74,6 @@ import { defineComponent, nextTick, type PropType } from "vue";
import { type MenuListEntry } from "@/wasm-communication/messages"; import { type MenuListEntry } from "@/wasm-communication/messages";
import MenuList from "@/components/floating-menus/MenuList.vue"; import MenuList from "@/components/floating-menus/MenuList.vue";
import type FloatingMenu from "@/components/layout/FloatingMenu.vue";
import type LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue"; import IconLabel from "@/components/widgets/labels/IconLabel.vue";
@ -105,29 +103,26 @@ export default defineComponent({
this.highlighted = this.activeEntry; this.highlighted = this.activeEntry;
}, },
methods: { methods: {
floatingMenu() { async setOpen(): Promise<void> {
return this.$refs.floatingMenu as typeof FloatingMenu;
},
scroller() {
return ((this.$refs.menulist as typeof MenuList).$refs.scroller as typeof LayoutCol)?.$el as HTMLElement;
},
async setOpen() {
this.open = true; this.open = true;
// Scroll to the active entry (the scroller div does not yet exist so we must wait for vue to render) // Scroll to the active entry (the scroller div does not yet exist so we must wait for Vue to render)
await nextTick(); await nextTick();
if (this.activeEntry) { if (this.activeEntry) {
const index = this.entries.indexOf(this.activeEntry); const index = this.entries.indexOf(this.activeEntry);
this.scroller()?.scrollTo(0, Math.max(0, index * 20 - 190)); (this.$refs.menuList as typeof MenuList | undefined)?.scrollViewTo(0, Math.max(0, index * 20 - 190));
} }
}, },
toggleOpen() { toggleOpen(): void {
if (this.disabled) return; if (!this.disabled) {
this.open = !this.open; this.open = !this.open;
if (this.open) this.setOpen();
if (this.open) this.setOpen();
}
}, },
keydown(e: KeyboardEvent) { keydown(e: KeyboardEvent): void {
(this.$refs.menulist as typeof MenuList).keydown(e, false); (this.$refs.menuList as typeof MenuList | undefined)?.keydown(e, false);
}, },
async selectFont(newName: string): Promise<void> { async selectFont(newName: string): Promise<void> {
let fontFamily; let fontFamily;

View File

@ -2,8 +2,8 @@
<div class="menu-bar-input" data-menu-bar-input> <div class="menu-bar-input" data-menu-bar-input>
<div class="entry-container" v-for="(entry, index) in entries" :key="index"> <div class="entry-container" v-for="(entry, index) in entries" :key="index">
<div <div
@click="(e: MouseEvent) => onClick(entry, e.target || undefined)" @click="(e: MouseEvent) => clickEntry(entry, e)"
@blur="(e: FocusEvent) => blur(entry, e.target || undefined)" @blur="(e: FocusEvent) => unFocusEntry(entry, e)"
@keydown="(e: KeyboardEvent) => entry.ref?.keydown(e, false)" @keydown="(e: KeyboardEvent) => entry.ref?.keydown(e, false)"
class="entry" class="entry"
:class="{ open: entry.ref?.isOpen }" :class="{ open: entry.ref?.isOpen }"
@ -20,7 +20,7 @@
:direction="'Bottom'" :direction="'Bottom'"
:minWidth="240" :minWidth="240"
:drawIcon="true" :drawIcon="true"
:ref="(ref: MenuListInstance) => ref && (entry.ref = ref)" :ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
/> />
</div> </div>
</div> </div>
@ -118,7 +118,7 @@ export default defineComponent({
}); });
}, },
methods: { methods: {
onClick(menuListEntry: MenuListEntry, target: EventTarget | undefined) { clickEntry(menuListEntry: MenuListEntry, e: MouseEvent) {
// If there's no menu to open, trigger the action but don't try to open its non-existant children // If there's no menu to open, trigger the action but don't try to open its non-existant children
if (!menuListEntry.children || menuListEntry.children.length === 0) { if (!menuListEntry.children || menuListEntry.children.length === 0) {
if (menuListEntry.action && !menuListEntry.disabled) menuListEntry.action(); if (menuListEntry.action && !menuListEntry.disabled) menuListEntry.action();
@ -127,13 +127,15 @@ export default defineComponent({
} }
// Focus the target so that keyboard inputs are sent to the dropdown // Focus the target so that keyboard inputs are sent to the dropdown
(target as HTMLElement)?.focus(); (e.target as HTMLElement | undefined)?.focus();
if (menuListEntry.ref) menuListEntry.ref.isOpen = true; if (menuListEntry.ref) menuListEntry.ref.isOpen = true;
else throw new Error("The menu bar floating menu has no associated ref"); else throw new Error("The menu bar floating menu has no associated ref");
}, },
blur(menuListEntry: MenuListEntry, target: EventTarget | undefined) { unFocusEntry(menuListEntry: MenuListEntry, e: FocusEvent) {
if ((target as HTMLElement)?.closest("[data-menu-bar-input]") !== this.$el && menuListEntry.ref) menuListEntry.ref.isOpen = false; const blurTarget = (e.target as HTMLElement | undefined)?.closest("[data-menu-bar-input]");
const self: HTMLDivElement | undefined = this.$el;
if (blurTarget !== self && menuListEntry.ref) menuListEntry.ref.isOpen = false;
}, },
}, },
data() { data() {

View File

@ -130,15 +130,12 @@ export default defineComponent({
this.editing = true; this.editing = true;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement; (this.$refs.fieldInput as typeof FieldInput | undefined)?.selectAllText(this.text);
// Setting the value directly is required to make `inputElement.select()` work
inputElement.value = this.text;
inputElement.select();
}, },
// Called only when `value` is changed from the <input> element via user input and committed, either with the // Called only when `value` is changed from the <input> element via user input and committed, either with the
// enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding) // enter key (via the `change` event) or when the <input> element is unfocused (with the `blur` event binding)
onTextChanged() { onTextChanged() {
// The `inputElement.blur()` call at the bottom of this function causes itself to be run again, so this check skips a second run // The `unFocus()` call at the bottom of this function and in `onCancelTextChange()` causes this function to be run again, so this check skips a second run
if (!this.editing) return; if (!this.editing) return;
const parsed = parseFloat(this.text); const parsed = parseFloat(this.text);
@ -148,16 +145,14 @@ export default defineComponent({
this.editing = false; this.editing = false;
const inputElement = (this.$refs.fieldInput as typeof FieldInput)?.$refs?.input as HTMLInputElement | undefined; (this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
inputElement?.blur();
}, },
onCancelTextChange() { onCancelTextChange() {
this.updateValue(undefined); this.updateValue(undefined);
this.editing = false; this.editing = false;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement; (this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
inputElement.blur();
}, },
onIncrement(direction: IncrementDirection) { onIncrement(direction: IncrementDirection) {
if (this.value === undefined) return; if (this.value === undefined) return;

View File

@ -50,25 +50,25 @@ export default defineComponent({
this.editing = true; this.editing = true;
}, },
// Called only when `value` is changed from the <textarea> element via user input and committed, either // Called only when `value` is changed from the <textarea> element via user input and committed, either
// via the `change` event or when the <input> element is defocused (with the `blur` event binding) // via the `change` event or when the <input> element is unfocused (with the `blur` event binding)
onTextChanged() { onTextChanged() {
// The `inputElement.blur()` call in `onCancelTextChange()` causes itself to be run again, so this if statement skips a second run // The `unFocus()` call in `onCancelTextChange()` causes itself to be run again, so this if statement skips a second run
if (!this.editing) return; if (!this.editing) return;
this.onCancelTextChange(); this.onCancelTextChange();
// TODO: Find a less hacky way to do this // TODO: Find a less hacky way to do this
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLTextAreaElement; const inputElement = this.$refs.fieldInput as typeof FieldInput | undefined;
this.$emit("commitText", inputElement.value); if (!inputElement) return;
this.$emit("commitText", inputElement.getInputElementValue());
// Required if value is not changed by the parent component upon update:value event // Required if value is not changed by the parent component upon update:value event
inputElement.value = this.value; inputElement.setInputElementValue(this.value);
}, },
onCancelTextChange() { onCancelTextChange() {
this.editing = false; this.editing = false;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLTextAreaElement; (this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
inputElement.blur();
}, },
}, },
components: { FieldInput }, components: { FieldInput },

View File

@ -55,31 +55,28 @@ export default defineComponent({
onTextFocused() { onTextFocused() {
this.editing = true; this.editing = true;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement; (this.$refs.fieldInput as typeof FieldInput | undefined)?.selectAllText(this.text);
// Setting the value directly is required to make `inputElement.select()` work
inputElement.value = this.text;
inputElement.select();
}, },
// Called only when `value` is changed from the <input> element via user input and committed, either with the // Called only when `value` is changed from the <input> element via user input and committed, either with the
// enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding) // enter key (via the `change` event) or when the <input> element is unfocused (with the `blur` event binding)
onTextChanged() { onTextChanged() {
// The `inputElement.blur()` call in `onCancelTextChange()` causes itself to be run again, so this if statement skips a second run // The `unFocus()` call in `onCancelTextChange()` causes itself to be run again, so this if statement skips a second run
if (!this.editing) return; if (!this.editing) return;
this.onCancelTextChange(); this.onCancelTextChange();
// TODO: Find a less hacky way to do this // TODO: Find a less hacky way to do this
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement; const inputElement = this.$refs.fieldInput as typeof FieldInput | undefined;
this.$emit("commitText", inputElement.value); if (!inputElement) return;
this.$emit("commitText", inputElement.getInputElementValue());
// Required if value is not changed by the parent component upon update:value event // Required if value is not changed by the parent component upon update:value event
inputElement.value = this.value; inputElement.setInputElementValue(this.value);
}, },
onCancelTextChange() { onCancelTextChange() {
this.editing = false; this.editing = false;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement; (this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
inputElement.blur();
}, },
}, },
components: { FieldInput }, components: { FieldInput },

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="canvas-ruler" :class="direction.toLowerCase()" ref="rulerRef"> <div class="canvas-ruler" :class="direction.toLowerCase()" ref="canvasRuler">
<svg :style="svgBounds"> <svg :style="svgBounds">
<path :d="svgPath" /> <path :d="svgPath" />
<text v-for="(svgText, index) in svgTexts" :key="index" :transform="svgText.transform">{{ svgText.text }}</text> <text v-for="(svgText, index) in svgTexts" :key="index" :transform="svgText.transform">{{ svgText.text }}</text>
@ -122,12 +122,12 @@ export default defineComponent({
}, },
methods: { methods: {
resize() { resize() {
if (!this.$refs.rulerRef) return; const canvasRuler = this.$refs.canvasRuler as HTMLDivElement | undefined;
if (!canvasRuler) return;
const rulerElement = this.$refs.rulerRef as HTMLElement;
const isVertical = this.direction === "Vertical"; const isVertical = this.direction === "Vertical";
const newLength = isVertical ? rulerElement.clientHeight : rulerElement.clientWidth; const newLength = isVertical ? canvasRuler.clientHeight : canvasRuler.clientWidth;
const roundedUp = (Math.floor(newLength / this.majorMarkSpacing) + 1) * this.majorMarkSpacing; const roundedUp = (Math.floor(newLength / this.majorMarkSpacing) + 1) * this.majorMarkSpacing;
if (roundedUp !== this.rulerLength) { if (roundedUp !== this.rulerLength) {

View File

@ -2,7 +2,7 @@
<div class="persistent-scrollbar" :class="direction.toLowerCase()"> <div class="persistent-scrollbar" :class="direction.toLowerCase()">
<button class="arrow decrease" @pointerdown="() => changePosition(-50)"></button> <button class="arrow decrease" @pointerdown="() => changePosition(-50)"></button>
<div class="scroll-track" ref="scrollTrack" @pointerdown="(e) => grabArea(e)"> <div class="scroll-track" ref="scrollTrack" @pointerdown="(e) => grabArea(e)">
<div class="scroll-thumb" @pointerdown="(e) => grabHandle(e)" :class="{ dragging }" ref="handle" :style="[thumbStart, thumbEnd, sides]"></div> <div class="scroll-thumb" @pointerdown="(e) => grabHandle(e)" :class="{ dragging }" :style="[thumbStart, thumbEnd, sides]"></div>
</div> </div>
<button class="arrow increase" @click="() => changePosition(50)"></button> <button class="arrow increase" @click="() => changePosition(50)"></button>
</div> </div>
@ -160,21 +160,29 @@ export default defineComponent({
window.removeEventListener("pointermove", this.pointerMove); window.removeEventListener("pointermove", this.pointerMove);
}, },
methods: { methods: {
trackLength(): number { trackLength(): number | undefined {
const track = this.$refs.scrollTrack as HTMLElement; const track = this.$refs.scrollTrack as HTMLDivElement | undefined;
return this.direction === "Vertical" ? track.clientHeight - this.handleLength : track.clientWidth; if (track) return this.direction === "Vertical" ? track.clientHeight - this.handleLength : track.clientWidth;
return undefined;
}, },
trackOffset(): number { trackOffset(): number | undefined {
const track = this.$refs.scrollTrack as HTMLElement; const track = this.$refs.scrollTrack as HTMLDivElement | undefined;
return this.direction === "Vertical" ? track.getBoundingClientRect().top : track.getBoundingClientRect().left; if (track) return this.direction === "Vertical" ? track.getBoundingClientRect().top : track.getBoundingClientRect().left;
return undefined;
}, },
clampHandlePosition(newPos: number) { clampHandlePosition(newPos: number) {
const clampedPosition = Math.min(Math.max(newPos, 0), 1); const clampedPosition = Math.min(Math.max(newPos, 0), 1);
this.$emit("update:handlePosition", clampedPosition); this.$emit("update:handlePosition", clampedPosition);
}, },
updateHandlePosition(e: PointerEvent) { updateHandlePosition(e: PointerEvent) {
const trackLength = this.trackLength();
if (trackLength === undefined) return;
const position = pointerPosition(this.direction, e); const position = pointerPosition(this.direction, e);
this.clampHandlePosition(this.handlePosition + (position - this.pointerPos) / (this.trackLength() * (1 - this.handleLength)));
this.clampHandlePosition(this.handlePosition + (position - this.pointerPos) / (trackLength * (1 - this.handleLength)));
this.pointerPos = position; this.pointerPos = position;
}, },
grabHandle(e: PointerEvent) { grabHandle(e: PointerEvent) {
@ -185,8 +193,12 @@ export default defineComponent({
}, },
grabArea(e: PointerEvent) { grabArea(e: PointerEvent) {
if (!this.dragging) { if (!this.dragging) {
const trackLength = this.trackLength();
const trackOffset = this.trackOffset();
if (trackLength === undefined || trackOffset === undefined) return;
const oldPointer = handleToTrack(this.handleLength, this.handlePosition) * trackLength + trackOffset;
const pointerPos = pointerPosition(this.direction, e); const pointerPos = pointerPosition(this.direction, e);
const oldPointer = handleToTrack(this.handleLength, this.handlePosition) * this.trackLength() + this.trackOffset();
this.$emit("pressTrack", pointerPos - oldPointer); this.$emit("pressTrack", pointerPos - oldPointer);
} }
}, },
@ -197,7 +209,10 @@ export default defineComponent({
if (this.dragging) this.updateHandlePosition(e); if (this.dragging) this.updateHandlePosition(e);
}, },
changePosition(difference: number) { changePosition(difference: number) {
this.clampHandlePosition(this.handlePosition + difference / this.trackLength()); const trackLength = this.trackLength();
if (trackLength === undefined) return;
this.clampHandlePosition(this.handlePosition + difference / trackLength);
}, },
}, },
}); });

View File

@ -1,6 +1,6 @@
<template> <template>
<LayoutCol class="panel"> <LayoutCol class="panel">
<LayoutRow class="tab-bar" data-tab-bar :class="{ 'min-widths': tabMinWidths }"> <LayoutRow class="tab-bar" :class="{ 'min-widths': tabMinWidths }">
<LayoutRow class="tab-group" :scrollableX="true"> <LayoutRow class="tab-group" :scrollableX="true">
<LayoutRow <LayoutRow
v-for="(tabLabel, tabIndex) in tabLabels" v-for="(tabLabel, tabIndex) in tabLabels"
@ -13,7 +13,7 @@
data-tab data-tab
> >
<span>{{ tabLabel.name }}</span> <span>{{ tabLabel.name }}</span>
<IconButton :action="(e: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" /> <IconButton :action="(e?: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
</LayoutRow> </LayoutRow>
</LayoutRow> </LayoutRow>
<PopoverButton :icon="'VerticalEllipsis'"> <PopoverButton :icon="'VerticalEllipsis'">
@ -210,7 +210,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { defineComponent, type PropType } from "vue"; import { defineComponent, nextTick, type PropType } from "vue";
import { platformIsMac } from "@/utility-functions/platform"; import { platformIsMac } from "@/utility-functions/platform";
@ -268,6 +268,15 @@ export default defineComponent({
if (platformIsMac()) return reservedKey ? [ALT, COMMAND] : [COMMAND]; if (platformIsMac()) return reservedKey ? [ALT, COMMAND] : [COMMAND];
return reservedKey ? [CONTROL, ALT] : [CONTROL]; return reservedKey ? [CONTROL, ALT] : [CONTROL];
}, },
async scrollTabIntoView(newIndex: number) {
await nextTick();
const panel: HTMLDivElement | undefined = this.$el;
if (!panel) return;
const newActiveTab = panel.querySelectorAll("[data-tab]")[newIndex] as HTMLDivElement | undefined;
newActiveTab?.scrollIntoView();
},
}, },
components: { components: {
IconLabel, IconLabel,

View File

@ -7,11 +7,11 @@
:panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined" :panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined"
:tabCloseButtons="true" :tabCloseButtons="true"
:tabMinWidths="true" :tabMinWidths="true"
:tabLabels="portfolio.state.documents.map((doc) => ({ name: doc.displayName, tooltip: doc.id }))" :tabLabels="documentTabLabels"
:clickAction="(tabIndex: number) => editor.instance.selectDocument(portfolio.state.documents[tabIndex].id)" :clickAction="(tabIndex: number) => editor.instance.selectDocument(portfolio.state.documents[tabIndex].id)"
:closeAction="(tabIndex: number) => editor.instance.closeDocumentWithConfirmation(portfolio.state.documents[tabIndex].id)" :closeAction="(tabIndex: number) => editor.instance.closeDocumentWithConfirmation(portfolio.state.documents[tabIndex].id)"
:tabActiveIndex="portfolio.state.activeDocumentIndex" :tabActiveIndex="portfolio.state.activeDocumentIndex"
ref="documentsPanel" ref="documentPanel"
/> />
</LayoutRow> </LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow> <LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
@ -64,7 +64,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { defineComponent, nextTick } from "vue"; import { defineComponent } from "vue";
import DialogModal from "@/components/floating-menus/DialogModal.vue"; import DialogModal from "@/components/floating-menus/DialogModal.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue"; import LayoutCol from "@/components/layout/LayoutCol.vue";
@ -82,12 +82,22 @@ export default defineComponent({
nodeGraphVisible() { nodeGraphVisible() {
return this.workspace.state.nodeGraphVisible; return this.workspace.state.nodeGraphVisible;
}, },
documentTabLabels() {
return this.portfolio.state.documents.map((doc) => {
const name = doc.displayName;
if (!this.editor.instance.inDevelopmentMode()) return { name };
const tooltip = `Document ID ${doc.id}`;
return { name, tooltip };
});
},
}, },
methods: { methods: {
resizePanel(event: PointerEvent) { resizePanel(event: PointerEvent) {
const gutter = event.target as HTMLElement; const gutter = event.target as HTMLDivElement;
const nextSibling = gutter.nextElementSibling as HTMLElement; const nextSibling = gutter.nextElementSibling as HTMLDivElement;
const previousSibling = gutter.previousElementSibling as HTMLElement; const previousSibling = gutter.previousElementSibling as HTMLDivElement;
// Are we resizing horizontally? // Are we resizing horizontally?
const horizontal = gutter.classList.contains("layout-col"); const horizontal = gutter.classList.contains("layout-col");
@ -129,11 +139,7 @@ export default defineComponent({
}, },
watch: { watch: {
async activeDocumentIndex(newIndex: number) { async activeDocumentIndex(newIndex: number) {
await nextTick(); (this.$refs.documentPanel as typeof Panel | undefined)?.scrollTabIntoView(newIndex);
const documentsPanel = this.$refs.documentsPanel as typeof Panel;
const newActiveTab = documentsPanel.$el.querySelectorAll("[data-tab-bar] [data-tab]")[newIndex];
newActiveTab.scrollIntoView();
}, },
}, },
components: { components: {

View File

@ -764,7 +764,7 @@ export type TextButtonWidget = {
props: { props: {
kind: "TextButton"; kind: "TextButton";
label: string; label: string;
icon?: string; icon?: IconName;
emphasized?: boolean; emphasized?: boolean;
minWidth?: number; minWidth?: number;
disabled?: boolean; disabled?: boolean;