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();
return {
draggingPickerTrack: undefined as HTMLElement | undefined,
draggingPickerTrack: undefined as HTMLDivElement | undefined,
hue: hsva.h,
saturation: hsva.s,
value: hsva.v,

View file

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

View file

@ -113,7 +113,9 @@ export default defineComponent({
},
methods: {
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.height = ZOOM_WINDOW_DIMENSIONS;
const context = canvas.getContext("2d");

View file

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

View file

@ -1,6 +1,6 @@
<template>
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" ref="floatingMenu">
<div class="tail" v-if="open && type === 'Popover'" ref="tail"></div>
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]">
<div class="tail" :style="tailStyle" v-if="displayTail"></div>
<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>
<slot></slot>
@ -203,6 +203,8 @@ export default defineComponent({
escapeCloses: { type: Boolean as PropType<boolean>, default: true },
},
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.
// 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.
@ -216,6 +218,7 @@ export default defineComponent({
measuringOngoing: false,
measuringOngoingGuard: false,
minWidthParentWidth: 0,
tailStyle,
containerResizeObserver,
pointerStillDown: false,
workspaceBounds: new DOMRect(),
@ -228,6 +231,9 @@ export default defineComponent({
if (this.measuringOngoing) return "0";
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
// TODO: Use the Vue :style attribute more whilst not causing recursive updates
@ -245,15 +251,15 @@ export default defineComponent({
if (this.type === "Cursor") return;
const workspace = document.querySelector("[data-workspace]");
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol;
const floatingMenuContent: HTMLElement | undefined = floatingMenuContentComponent?.$el;
const floatingMenu = this.$refs.floatingMenu as HTMLElement;
const floatingMenu: HTMLDivElement | undefined = this.$el;
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLDivElement | undefined;
const floatingMenuContent: HTMLDivElement | undefined = (this.$refs.floatingMenuContent as typeof LayoutCol | undefined)?.$el;
if (!workspace || !floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !floatingMenu) return;
if (!workspace || !floatingMenu || !floatingMenuContainer || !floatingMenuContent) return;
this.workspaceBounds = workspace.getBoundingClientRect();
this.floatingMenuBounds = floatingMenu.getBoundingClientRect();
const floatingMenuContainerBounds = floatingMenuContainer.getBoundingClientRect();
this.floatingMenuContentBounds = floatingMenuContent.getBoundingClientRect();
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`;
// Required to correctly position tail when scrolled (it has a `position: fixed` to prevent clipping)
const tail = this.$refs.tail as HTMLElement;
if (tail) {
if (this.direction === "Bottom") tail.style.top = `${this.floatingMenuBounds.top}px`;
if (this.direction === "Top") tail.style.bottom = `${this.floatingMenuBounds.bottom}px`;
if (this.direction === "Right") tail.style.left = `${this.floatingMenuBounds.left}px`;
if (this.direction === "Left") tail.style.right = `${this.floatingMenuBounds.right}px`;
}
if (this.direction === "Bottom") this.tailStyle = { top: `${this.floatingMenuBounds.top}px` };
if (this.direction === "Top") this.tailStyle = { bottom: `${this.floatingMenuBounds.bottom}px` };
if (this.direction === "Right") this.tailStyle = { left: `${this.floatingMenuBounds.left}px` };
if (this.direction === "Left") this.tailStyle = { right: `${this.floatingMenuBounds.right}px` };
}
type Edge = "Top" | "Bottom" | "Left" | "Right";
@ -285,11 +288,11 @@ export default defineComponent({
if (this.floatingMenuContentBounds.left - this.windowEdgeMargin <= this.workspaceBounds.left) {
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) {
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") {
@ -297,11 +300,11 @@ export default defineComponent({
if (this.floatingMenuContentBounds.top - this.windowEdgeMargin <= this.workspaceBounds.top) {
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) {
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
this.measuringOngoing = true;
this.measuringOngoingGuard = true;
await nextTick();
// Only measure if the menu is visible, perhaps because a parent component with a `v-if` condition is false
let naturalWidth;
if (this.$refs.floatingMenuContent) {
// Measure the width of the floating menu content element
const floatingMenuContent: HTMLElement = (this.$refs.floatingMenuContent as typeof LayoutCol).$el;
naturalWidth = floatingMenuContent?.clientWidth;
}
// Measure the width of the floating menu content element, if it's currently visible
// The result will be `undefined` if the menu is invisible, perhaps because an ancestor component is hidden with a falsy `v-if` condition
const floatingMenuContent: HTMLDivElement | undefined = (this.$refs.floatingMenuContent as typeof LayoutCol | undefined)?.$el;
const 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
this.measuringOngoing = false;
@ -363,7 +362,8 @@ export default defineComponent({
const target = e.target 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 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
if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) {
@ -418,10 +418,13 @@ export default defineComponent({
},
isPointerEventOutsideFloatingMenu(e: PointerEvent, extraDistanceAllowed = 0): boolean {
// 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));
},
isPointerEventOutsideMenuElement(e: PointerEvent, element: HTMLElement, extraDistanceAllowed = 0): boolean {
isPointerEventOutsideMenuElement(e: PointerEvent, element: Element, extraDistanceAllowed = 0): boolean {
const floatingMenuBounds = element.getBoundingClientRect();
if (floatingMenuBounds.left - e.clientX >= extraDistanceAllowed) return true;
@ -450,7 +453,7 @@ export default defineComponent({
await nextTick();
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLDivElement | undefined;
if (!floatingMenuContainer) return;
// Start a new observation of the now-open floating menu

View file

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

View file

@ -4,7 +4,7 @@
<WidgetLayout :layout="layerTreeOptionsLayout" />
</LayoutRow>
<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
class="layer-row"
v-for="(listing, index) in layers"
@ -13,7 +13,7 @@
>
<LayoutRow class="visibility">
<IconButton
:action="(e: MouseEvent) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())"
:action="(e?: MouseEvent) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())"
:size="24"
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
:title="listing.entry.visible ? 'Visible' : 'Hidden'"
@ -56,8 +56,8 @@
:disabled="!listing.editingName"
@blur="() => onEditLayerNameDeselect(listing)"
@keydown.esc="onEditLayerNameDeselect(listing)"
@keydown.enter="(e) => onEditLayerNameChange(listing, e.target || undefined)"
@change="(e) => onEditLayerNameChange(listing, e.target || undefined)"
@keydown.enter="(e) => onEditLayerNameChange(listing, e)"
@change="(e) => onEditLayerNameChange(listing, e)"
/>
</LayoutRow>
<div class="thumbnail" v-html="listing.entry.thumbnail"></div>
@ -326,23 +326,24 @@ export default defineComponent({
async onEditLayerName(listing: LayerListingInfo) {
if (listing.editingName) return;
listing.editingName = true;
this.draggable = false;
listing.editingName = true;
const tree: HTMLElement = (this.$refs.layerTreeList as typeof LayoutCol).$el;
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
if (!listing.editingName) return;
this.draggable = true;
const name = (inputElement as HTMLInputElement).value;
const name = (e.target as HTMLInputElement | undefined)?.value;
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) {
this.draggable = true;
@ -368,7 +369,7 @@ export default defineComponent({
async deselectAllLayers() {
this.editor.instance.deselectAllLayers();
},
calculateDragIndex(tree: HTMLElement, clientY: number): DraggingData {
calculateDragIndex(tree: HTMLDivElement, clientY: number): DraggingData {
const treeChildren = tree.children;
const treeOffset = tree.getBoundingClientRect().top;
@ -438,16 +439,16 @@ export default defineComponent({
event.dataTransfer.dropEffect = "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) {
// Stop the drag from being shown as cancelled
event.preventDefault();
const tree: HTMLElement = (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);
},
async drop() {
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="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="raster">
<!-- <div class="input port" data-port="input" data-datatype="raster">
<div></div>
</div> -->
<div class="output port" data-datatype="raster">
<div class="output port" data-port="output" data-datatype="raster">
<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="primary">
<div class="ports">
<div class="input port" data-datatype="raster">
<div class="input port" data-port="input" data-datatype="raster">
<div></div>
</div>
<div class="output port" data-datatype="raster">
<div class="output port" data-port="output" data-datatype="raster">
<div></div>
</div>
</div>
@ -48,10 +48,10 @@
<div class="arguments">
<div class="argument">
<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 class="output port" data-datatype="raster">
<!-- <div class="output port" data-port="output" data-datatype="raster">
<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="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="raster">
<!-- <div class="input port" data-port="input" data-datatype="raster">
<div></div>
</div> -->
<div class="output port" data-datatype="raster">
<div class="output port" data-port="output" data-datatype="raster">
<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="primary">
<div class="ports">
<div class="input port" data-datatype="raster">
<div class="input port" data-port="input" data-datatype="raster">
<div></div>
</div>
<div class="output port" data-datatype="raster">
<div class="output port" data-port="output" data-datatype="raster">
<div></div>
</div>
</div>
@ -89,10 +89,10 @@
<div class="arguments">
<div class="argument">
<div class="ports">
<div class="input port" data-datatype="raster">
<div class="input port" data-port="input" data-datatype="raster">
<div></div>
</div>
<!-- <div class="output port" data-datatype="raster">
<!-- <div class="output port" data-port="output" data-datatype="raster">
<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="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="vector">
<!-- <div class="input port" data-port="input" data-datatype="vector">
<div></div>
</div> -->
<div class="output port" data-datatype="vector">
<div class="output port" data-port="output" data-datatype="vector">
<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="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="raster">
<!-- <div class="input port" data-port="input" data-datatype="raster">
<div></div>
</div> -->
<div class="output port" data-datatype="raster">
<div class="output port" data-port="output" data-datatype="raster">
<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="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="raster">
<!-- <div class="input port" data-port="input" data-datatype="raster">
<div></div>
</div> -->
<div class="output port" data-datatype="raster">
<div class="output port" data-port="output" data-datatype="raster">
<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="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="raster">
<!-- <div class="input port" data-port="input" data-datatype="raster">
<div></div>
</div> -->
<div class="output port" data-datatype="raster">
<div class="output port" data-port="output" data-datatype="raster">
<div></div>
</div>
</div>
@ -355,7 +355,7 @@ export default defineComponent({
return {
transform: { scale: 1, x: 0, y: 0 },
panning: false,
drawing: undefined as { port: HTMLElement; output: boolean; path: SVGElement } | undefined,
drawing: undefined as { port: HTMLDivElement; output: boolean; path: SVGElement } | undefined,
};
},
computed: {
@ -375,7 +375,8 @@ export default defineComponent({
},
methods: {
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 outY = verticalOut ? outputBounds.y + 1 : outputBounds.y + outputBounds.height / 2;
@ -402,14 +403,14 @@ export default defineComponent({
verticalIn ? inConnectorX : inConnectorX - horizontalCurve
},${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 dataType = outputPort.dataset.datatype;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", pathString);
path.setAttribute("style", `--data-color: var(--color-data-${dataType}); --data-color-dim: var(--color-data-${dataType}-dim)`);
(this.$refs.wiresContainer as HTMLElement).appendChild(path);
path.setAttribute("style", `--data-color: var(--color-data-${dataType}); --data-color-dim: var(--color-data-${dataType}-dim)`);
(this.$refs.wiresContainer as SVGSVGElement | undefined)?.appendChild(path);
return path;
},
@ -418,7 +419,9 @@ export default defineComponent({
let zoomFactor = 1 + Math.abs(scroll) * WHEEL_RATE;
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;
@ -435,7 +438,7 @@ export default defineComponent({
this.transform.y -= (deltaY / this.transform.scale) * zoomFactor;
},
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) {
const output = port.classList.contains("output");
@ -444,7 +447,9 @@ export default defineComponent({
} else {
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) {
if (this.panning) {
@ -461,19 +466,20 @@ export default defineComponent({
}
},
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.drawing = undefined;
},
},
mounted() {
const outputPort1 = document.querySelectorAll(".output.port")[4] as HTMLElement;
const inputPort1 = document.querySelectorAll(".input.port")[1] as HTMLElement;
this.createWirePath(outputPort1, inputPort1, true, true);
const outputPort1 = document.querySelectorAll(`[data-port="${"output"}"]`)[4] as HTMLDivElement | undefined;
const inputPort1 = document.querySelectorAll(`[data-port="${"input"}"]`)[1] as HTMLDivElement | undefined;
if (outputPort1 && inputPort1) this.createWirePath(outputPort1, inputPort1, true, true);
const outputPort2 = document.querySelectorAll(".output.port")[6] as HTMLElement;
const inputPort2 = document.querySelectorAll(".input.port")[3] as HTMLElement;
this.createWirePath(outputPort2, inputPort2, true, false);
const outputPort2 = document.querySelectorAll(`[data-port="${"output"}"]`)[6] as HTMLDivElement | undefined;
const inputPort2 = document.querySelectorAll(`[data-port="${"input"}"]`)[3] as HTMLDivElement | undefined;
if (outputPort2 && inputPort2) this.createWirePath(outputPort2, inputPort2, true, false);
},
components: {
IconLabel,

View file

@ -1,7 +1,7 @@
<template>
<LayoutRow class="checkbox-input">
<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">
<IconLabel :icon="icon" />
</LayoutRow>
@ -80,6 +80,11 @@ export default defineComponent({
isChecked() {
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: {
IconLabel,

View file

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

View file

@ -140,6 +140,28 @@ export default defineComponent({
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: {
inputValue: {
get() {

View file

@ -5,7 +5,6 @@
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
</LayoutRow>
<MenuList
ref="menulist"
v-model:activeEntry="activeEntry"
v-model:open="open"
:entries="[entries]"
@ -13,6 +12,7 @@
:virtualScrollingEntryHeight="isStyle ? 0 : 20"
:scrollableY="true"
@naturalWidth="(newNaturalWidth: number) => (isStyle && (minWidth = newNaturalWidth))"
ref="menuList"
></MenuList>
</LayoutRow>
</template>
@ -74,8 +74,6 @@ import { defineComponent, nextTick, type PropType } from "vue";
import { type MenuListEntry } from "@/wasm-communication/messages";
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 IconLabel from "@/components/widgets/labels/IconLabel.vue";
@ -105,29 +103,26 @@ export default defineComponent({
this.highlighted = this.activeEntry;
},
methods: {
floatingMenu() {
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() {
async setOpen(): Promise<void> {
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();
if (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() {
if (this.disabled) return;
this.open = !this.open;
if (this.open) this.setOpen();
toggleOpen(): void {
if (!this.disabled) {
this.open = !this.open;
if (this.open) this.setOpen();
}
},
keydown(e: KeyboardEvent) {
(this.$refs.menulist as typeof MenuList).keydown(e, false);
keydown(e: KeyboardEvent): void {
(this.$refs.menuList as typeof MenuList | undefined)?.keydown(e, false);
},
async selectFont(newName: string): Promise<void> {
let fontFamily;

View file

@ -2,8 +2,8 @@
<div class="menu-bar-input" data-menu-bar-input>
<div class="entry-container" v-for="(entry, index) in entries" :key="index">
<div
@click="(e: MouseEvent) => onClick(entry, e.target || undefined)"
@blur="(e: FocusEvent) => blur(entry, e.target || undefined)"
@click="(e: MouseEvent) => clickEntry(entry, e)"
@blur="(e: FocusEvent) => unFocusEntry(entry, e)"
@keydown="(e: KeyboardEvent) => entry.ref?.keydown(e, false)"
class="entry"
:class="{ open: entry.ref?.isOpen }"
@ -20,7 +20,7 @@
:direction="'Bottom'"
:minWidth="240"
:drawIcon="true"
:ref="(ref: MenuListInstance) => ref && (entry.ref = ref)"
:ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
/>
</div>
</div>
@ -118,7 +118,7 @@ export default defineComponent({
});
},
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 (!menuListEntry.children || menuListEntry.children.length === 0) {
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
(target as HTMLElement)?.focus();
(e.target as HTMLElement | undefined)?.focus();
if (menuListEntry.ref) menuListEntry.ref.isOpen = true;
else throw new Error("The menu bar floating menu has no associated ref");
},
blur(menuListEntry: MenuListEntry, target: EventTarget | undefined) {
if ((target as HTMLElement)?.closest("[data-menu-bar-input]") !== this.$el && menuListEntry.ref) menuListEntry.ref.isOpen = false;
unFocusEntry(menuListEntry: MenuListEntry, e: FocusEvent) {
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() {

View file

@ -130,15 +130,12 @@ export default defineComponent({
this.editing = true;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
// Setting the value directly is required to make `inputElement.select()` work
inputElement.value = this.text;
inputElement.select();
(this.$refs.fieldInput as typeof FieldInput | undefined)?.selectAllText(this.text);
},
// 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() {
// 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;
const parsed = parseFloat(this.text);
@ -148,16 +145,14 @@ export default defineComponent({
this.editing = false;
const inputElement = (this.$refs.fieldInput as typeof FieldInput)?.$refs?.input as HTMLInputElement | undefined;
inputElement?.blur();
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
},
onCancelTextChange() {
this.updateValue(undefined);
this.editing = false;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
inputElement.blur();
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
},
onIncrement(direction: IncrementDirection) {
if (this.value === undefined) return;

View file

@ -50,25 +50,25 @@ export default defineComponent({
this.editing = true;
},
// 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() {
// 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;
this.onCancelTextChange();
// TODO: Find a less hacky way to do this
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLTextAreaElement;
this.$emit("commitText", inputElement.value);
const inputElement = this.$refs.fieldInput as typeof FieldInput | undefined;
if (!inputElement) return;
this.$emit("commitText", inputElement.getInputElementValue());
// Required if value is not changed by the parent component upon update:value event
inputElement.value = this.value;
inputElement.setInputElementValue(this.value);
},
onCancelTextChange() {
this.editing = false;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLTextAreaElement;
inputElement.blur();
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
},
},
components: { FieldInput },

View file

@ -55,31 +55,28 @@ export default defineComponent({
onTextFocused() {
this.editing = true;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
// Setting the value directly is required to make `inputElement.select()` work
inputElement.value = this.text;
inputElement.select();
(this.$refs.fieldInput as typeof FieldInput | undefined)?.selectAllText(this.text);
},
// 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() {
// 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;
this.onCancelTextChange();
// TODO: Find a less hacky way to do this
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
this.$emit("commitText", inputElement.value);
const inputElement = this.$refs.fieldInput as typeof FieldInput | undefined;
if (!inputElement) return;
this.$emit("commitText", inputElement.getInputElementValue());
// Required if value is not changed by the parent component upon update:value event
inputElement.value = this.value;
inputElement.setInputElementValue(this.value);
},
onCancelTextChange() {
this.editing = false;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
inputElement.blur();
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
},
},
components: { FieldInput },

View file

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

View file

@ -2,7 +2,7 @@
<div class="persistent-scrollbar" :class="direction.toLowerCase()">
<button class="arrow decrease" @pointerdown="() => changePosition(-50)"></button>
<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>
<button class="arrow increase" @click="() => changePosition(50)"></button>
</div>
@ -160,21 +160,29 @@ export default defineComponent({
window.removeEventListener("pointermove", this.pointerMove);
},
methods: {
trackLength(): number {
const track = this.$refs.scrollTrack as HTMLElement;
return this.direction === "Vertical" ? track.clientHeight - this.handleLength : track.clientWidth;
trackLength(): number | undefined {
const track = this.$refs.scrollTrack as HTMLDivElement | undefined;
if (track) return this.direction === "Vertical" ? track.clientHeight - this.handleLength : track.clientWidth;
return undefined;
},
trackOffset(): number {
const track = this.$refs.scrollTrack as HTMLElement;
return this.direction === "Vertical" ? track.getBoundingClientRect().top : track.getBoundingClientRect().left;
trackOffset(): number | undefined {
const track = this.$refs.scrollTrack as HTMLDivElement | undefined;
if (track) return this.direction === "Vertical" ? track.getBoundingClientRect().top : track.getBoundingClientRect().left;
return undefined;
},
clampHandlePosition(newPos: number) {
const clampedPosition = Math.min(Math.max(newPos, 0), 1);
this.$emit("update:handlePosition", clampedPosition);
},
updateHandlePosition(e: PointerEvent) {
const trackLength = this.trackLength();
if (trackLength === undefined) return;
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;
},
grabHandle(e: PointerEvent) {
@ -185,8 +193,12 @@ export default defineComponent({
},
grabArea(e: PointerEvent) {
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 oldPointer = handleToTrack(this.handleLength, this.handlePosition) * this.trackLength() + this.trackOffset();
this.$emit("pressTrack", pointerPos - oldPointer);
}
},
@ -197,7 +209,10 @@ export default defineComponent({
if (this.dragging) this.updateHandlePosition(e);
},
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>
<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
v-for="(tabLabel, tabIndex) in tabLabels"
@ -13,7 +13,7 @@
data-tab
>
<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>
<PopoverButton :icon="'VerticalEllipsis'">
@ -210,7 +210,7 @@
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { defineComponent, nextTick, type PropType } from "vue";
import { platformIsMac } from "@/utility-functions/platform";
@ -268,6 +268,15 @@ export default defineComponent({
if (platformIsMac()) return reservedKey ? [ALT, COMMAND] : [COMMAND];
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: {
IconLabel,

View file

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

View file

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