mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-20 11:28:30 +08:00
Move scrollable behavior into LayoutRow/LayoutCol
This commit is contained in:
@@ -2,14 +2,14 @@
|
||||
<div class="dialog-modal">
|
||||
<FloatingMenu :type="'Dialog'" :direction="'Center'">
|
||||
<LayoutRow>
|
||||
<LayoutCol :class="'icon-column'">
|
||||
<LayoutCol class="icon-column">
|
||||
<!-- `dialog.state.icon` class exists to provide special sizing in CSS to specific icons -->
|
||||
<IconLabel :icon="dialog.state.icon" :class="dialog.state.icon.toLowerCase()" />
|
||||
</LayoutCol>
|
||||
<LayoutCol :class="'main-column'">
|
||||
<TextLabel :bold="true" :class="'heading'">{{ dialog.state.heading }}</TextLabel>
|
||||
<TextLabel :class="'details'">{{ dialog.state.details }}</TextLabel>
|
||||
<LayoutRow :class="'buttons-row'" v-if="dialog.state.buttons.length > 0">
|
||||
<LayoutCol class="main-column">
|
||||
<TextLabel :bold="true" class="heading">{{ dialog.state.heading }}</TextLabel>
|
||||
<TextLabel class="details">{{ dialog.state.details }}</TextLabel>
|
||||
<LayoutRow class="buttons-row" v-if="dialog.state.buttons.length > 0">
|
||||
<TextButton v-for="(button, index) in dialog.state.buttons" :key="index" :title="button.tooltip" :action="() => button.callback && button.callback()" v-bind="button.props" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open || type === 'Dialog'" ref="floatingMenu">
|
||||
<div class="tail" v-if="type === 'Popover'"></div>
|
||||
<div class="floating-menu-container" ref="floatingMenuContainer">
|
||||
<div class="floating-menu-content" :class="{ 'scrollable-y': scrollable }" ref="floatingMenuContent" :style="floatingMenuContentStyle">
|
||||
<LayoutCol class="floating-menu-content" :scrollableY="scrollableY" ref="floatingMenuContent" :style="floatingMenuContentStyle">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</LayoutCol>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -179,6 +179,8 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, StyleValue } from "vue";
|
||||
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
|
||||
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center";
|
||||
export type MenuType = "Popover" | "Dropdown" | "Dialog";
|
||||
|
||||
@@ -190,14 +192,13 @@ export default defineComponent({
|
||||
type: { type: String as PropType<MenuType>, required: true },
|
||||
windowEdgeMargin: { type: Number as PropType<number>, default: 6 },
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
scrollable: { type: Boolean as PropType<boolean>, default: false },
|
||||
scrollableY: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
const containerResizeObserver = new ResizeObserver((entries) => {
|
||||
const content = entries[0].target.querySelector(".floating-menu-content") as HTMLElement;
|
||||
content.style.minWidth = `${entries[0].contentRect.width}px`;
|
||||
});
|
||||
|
||||
return {
|
||||
open: false,
|
||||
pointerStillDown: false,
|
||||
@@ -206,9 +207,11 @@ export default defineComponent({
|
||||
},
|
||||
updated() {
|
||||
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
|
||||
const floatingMenuContent = this.$refs.floatingMenuContent as HTMLElement;
|
||||
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol;
|
||||
const floatingMenuContent = floatingMenuContentComponent && (floatingMenuContentComponent.$el as HTMLElement);
|
||||
const workspace = document.querySelector(".workspace-row");
|
||||
if (!floatingMenuContainer || !floatingMenuContent || !workspace) return;
|
||||
|
||||
if (!floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !workspace) return;
|
||||
|
||||
const workspaceBounds = workspace.getBoundingClientRect();
|
||||
const floatingMenuBounds = floatingMenuContent.getBoundingClientRect();
|
||||
@@ -224,13 +227,11 @@ export default defineComponent({
|
||||
floatingMenuContent.style.left = `${this.windowEdgeMargin}px`;
|
||||
if (workspaceBounds.left + floatingMenuContainer.getBoundingClientRect().left === 12) zeroedBorderDirection2 = "Left";
|
||||
}
|
||||
|
||||
if (floatingMenuBounds.right + this.windowEdgeMargin >= workspaceBounds.right) {
|
||||
floatingMenuContent.style.right = `${this.windowEdgeMargin}px`;
|
||||
if (workspaceBounds.right - floatingMenuContainer.getBoundingClientRect().right === 12) zeroedBorderDirection2 = "Right";
|
||||
}
|
||||
}
|
||||
|
||||
if (this.direction === "Left" || this.direction === "Right") {
|
||||
zeroedBorderDirection2 = this.direction === "Left" ? "Right" : "Left";
|
||||
|
||||
@@ -238,7 +239,6 @@ export default defineComponent({
|
||||
floatingMenuContent.style.top = `${this.windowEdgeMargin}px`;
|
||||
if (workspaceBounds.top + floatingMenuContainer.getBoundingClientRect().top === 12) zeroedBorderDirection1 = "Top";
|
||||
}
|
||||
|
||||
if (floatingMenuBounds.bottom + this.windowEdgeMargin >= workspaceBounds.bottom) {
|
||||
floatingMenuContent.style.bottom = `${this.windowEdgeMargin}px`;
|
||||
if (workspaceBounds.bottom - floatingMenuContainer.getBoundingClientRect().bottom === 12) zeroedBorderDirection1 = "Bottom";
|
||||
@@ -277,23 +277,21 @@ export default defineComponent({
|
||||
},
|
||||
getWidth(callback: (width: number) => void) {
|
||||
this.$nextTick(() => {
|
||||
const floatingMenuContent = this.$refs.floatingMenuContent as HTMLElement;
|
||||
const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement;
|
||||
const width = floatingMenuContent.clientWidth;
|
||||
|
||||
callback(width);
|
||||
});
|
||||
},
|
||||
disableMinWidth(callback: (minWidth: string) => void) {
|
||||
this.$nextTick(() => {
|
||||
const floatingMenuContent = this.$refs.floatingMenuContent as HTMLElement;
|
||||
const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement;
|
||||
const initialMinWidth = floatingMenuContent.style.minWidth;
|
||||
floatingMenuContent.style.minWidth = "0";
|
||||
|
||||
callback(initialMinWidth);
|
||||
});
|
||||
},
|
||||
enableMinWidth(minWidth: string) {
|
||||
const floatingMenuContent = this.$refs.floatingMenuContent as HTMLElement;
|
||||
const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement;
|
||||
floatingMenuContent.style.minWidth = minWidth;
|
||||
},
|
||||
pointerMoveHandler(e: PointerEvent) {
|
||||
@@ -303,22 +301,18 @@ export default defineComponent({
|
||||
// TODO: Simplify the following expression when optional chaining is supported by the build system
|
||||
const pointerOverOwnFloatingMenuSpawner =
|
||||
pointerOverFloatingMenuSpawner && pointerOverFloatingMenuSpawner.parentElement && pointerOverFloatingMenuSpawner.parentElement.contains(this.$refs.floatingMenu as HTMLElement);
|
||||
|
||||
// Swap this open floating menu with the one created by the floating menu spawner being hovered over
|
||||
if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) {
|
||||
this.setClosed();
|
||||
pointerOverFloatingMenuSpawner.click();
|
||||
}
|
||||
|
||||
// Close the floating menu if the pointer has strayed far enough from its bounds
|
||||
if (this.isPointerEventOutsideFloatingMenu(e, POINTER_STRAY_DISTANCE) && !pointerOverOwnFloatingMenuSpawner && !pointerOverFloatingMenuKeepOpen) {
|
||||
// TODO: Extend this rectangle bounds check to all `data-hover-menu-keep-open` element bounds up the DOM tree since currently
|
||||
// submenus disappear with zero stray distance if the cursor is further than the stray distance from only the top-level menu
|
||||
this.setClosed();
|
||||
}
|
||||
|
||||
const eventIncludesLmb = Boolean(e.buttons & 1);
|
||||
|
||||
// Clean up any messes from lost pointerup events
|
||||
if (!this.open && !eventIncludesLmb) {
|
||||
this.pointerStillDown = false;
|
||||
@@ -329,7 +323,6 @@ export default defineComponent({
|
||||
// Close the floating menu if the pointer clicked outside the floating menu (but within stray distance)
|
||||
if (this.isPointerEventOutsideFloatingMenu(e)) {
|
||||
this.setClosed();
|
||||
|
||||
// Track if the left pointer button is now down so its later click event can be canceled
|
||||
const eventIsForLmb = e.button === 0;
|
||||
if (eventIsForLmb) this.pointerStillDown = true;
|
||||
@@ -337,12 +330,10 @@ export default defineComponent({
|
||||
},
|
||||
pointerUpHandler(e: PointerEvent) {
|
||||
const eventIsForLmb = e.button === 0;
|
||||
|
||||
if (this.pointerStillDown && eventIsForLmb) {
|
||||
// Clean up self
|
||||
this.pointerStillDown = false;
|
||||
window.removeEventListener("pointerup", this.pointerUpHandler);
|
||||
|
||||
// Prevent the click event from firing, which would normally occur right after this pointerup event
|
||||
window.addEventListener("click", this.clickHandlerCapture, true);
|
||||
}
|
||||
@@ -350,7 +341,6 @@ export default defineComponent({
|
||||
clickHandlerCapture(e: MouseEvent) {
|
||||
// Stop the click event from reopening this floating menu if the click event targets the floating menu's button
|
||||
e.stopPropagation();
|
||||
|
||||
// Clean up self
|
||||
window.removeEventListener("click", this.clickHandlerCapture, true);
|
||||
},
|
||||
@@ -374,13 +364,10 @@ export default defineComponent({
|
||||
if (newState && !oldState) {
|
||||
// Close floating menu if pointer strays far enough away
|
||||
window.addEventListener("pointermove", this.pointerMoveHandler);
|
||||
|
||||
// Close floating menu if pointer is outside (but within stray distance)
|
||||
window.addEventListener("pointerdown", this.pointerDownHandler);
|
||||
|
||||
// Cancel the subsequent click event to prevent the floating menu from reopening if the floating menu's button is the click event target
|
||||
window.addEventListener("pointerup", this.pointerUpHandler);
|
||||
|
||||
// Floating menu min-width resize observer
|
||||
this.$nextTick(() => {
|
||||
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
|
||||
@@ -390,12 +377,10 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Switching from open to closed
|
||||
if (!newState && oldState) {
|
||||
window.removeEventListener("pointermove", this.pointerMoveHandler);
|
||||
window.removeEventListener("pointerdown", this.pointerDownHandler);
|
||||
|
||||
this.containerResizeObserver.disconnect();
|
||||
}
|
||||
},
|
||||
@@ -407,5 +392,6 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
},
|
||||
components: { LayoutCol },
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<FloatingMenu :class="'menu-list'" :direction="direction" :type="'Dropdown'" ref="floatingMenu" :windowEdgeMargin="0" :scrollable="scrollable" data-hover-menu-keep-open>
|
||||
<FloatingMenu class="menu-list" :direction="direction" :type="'Dropdown'" ref="floatingMenu" :windowEdgeMargin="0" :scrollableY="scrollableY" data-hover-menu-keep-open>
|
||||
<template v-for="(section, sectionIndex) in menuEntries" :key="sectionIndex">
|
||||
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
|
||||
<div
|
||||
@@ -12,8 +12,8 @@
|
||||
@pointerleave="handleEntryPointerLeave(entry)"
|
||||
:data-hover-menu-spawner-extend="entry.children && []"
|
||||
>
|
||||
<CheckboxInput v-if="entry.checkbox" v-model:checked="entry.checked" :outlineStyle="true" :class="'entry-checkbox'" />
|
||||
<IconLabel v-else-if="entry.icon && drawIcon" :icon="entry.icon" :class="'entry-icon'" />
|
||||
<CheckboxInput v-if="entry.checkbox" v-model:checked="entry.checked" :outlineStyle="true" class="entry-checkbox" />
|
||||
<IconLabel v-else-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
|
||||
<div v-else-if="drawIcon" class="no-icon"></div>
|
||||
|
||||
<span class="entry-label">{{ entry.label }}</span>
|
||||
@@ -28,7 +28,7 @@
|
||||
v-if="entry.children"
|
||||
:direction="'TopRight'"
|
||||
:menuEntries="entry.children"
|
||||
v-bind="{ defaultAction, minWidth, drawIcon, scrollable }"
|
||||
v-bind="{ defaultAction, minWidth, drawIcon, scrollableY }"
|
||||
:ref="(ref: any) => setEntryRefs(entry, ref)"
|
||||
/>
|
||||
</div>
|
||||
@@ -168,7 +168,7 @@ const MenuList = defineComponent({
|
||||
defaultAction: { type: Function as PropType<() => void>, required: false },
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
drawIcon: { type: Boolean as PropType<boolean>, default: false },
|
||||
scrollable: { type: Boolean as PropType<boolean>, default: false },
|
||||
scrollableY: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
methods: {
|
||||
setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="dropdown-input">
|
||||
<div class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" @click="() => clickDropdownBox()" data-hover-menu-spawner>
|
||||
<IconLabel :class="'dropdown-icon'" :icon="activeEntry.icon" v-if="activeEntry.icon" />
|
||||
<IconLabel class="dropdown-icon" :icon="activeEntry.icon" v-if="activeEntry.icon" />
|
||||
<span>{{ activeEntry.label }}</span>
|
||||
<IconLabel :class="'dropdown-arrow'" :icon="'DropdownArrow'" />
|
||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||
</div>
|
||||
<MenuList
|
||||
v-model:activeEntry="activeEntry"
|
||||
@@ -12,7 +12,7 @@
|
||||
:menuEntries="menuEntries"
|
||||
:direction="'Bottom'"
|
||||
:drawIcon="drawIcon"
|
||||
:scrollable="true"
|
||||
:scrollableY="true"
|
||||
ref="menuList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user