Clean up Vue component refs (#813)
* Clean up Vue component refs * Second pass of code improvements
This commit is contained in:
parent
cee1add3a4
commit
d2e23d6b15
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue