mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-03 21:08:18 +00:00
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
21 changed files with 253 additions and 195 deletions
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -764,7 +764,7 @@ export type TextButtonWidget = {
|
|||
props: {
|
||||
kind: "TextButton";
|
||||
label: string;
|
||||
icon?: string;
|
||||
icon?: IconName;
|
||||
emphasized?: boolean;
|
||||
minWidth?: number;
|
||||
disabled?: boolean;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue