Generalize and rename overlays as "floating menus"
Progress towards #135.
This commit is contained in:
parent
c75478299a
commit
3d646d2bc3
|
|
@ -26,7 +26,7 @@
|
||||||
// TODO: Replace with CSS color() function to calculate alpha when browsers support it
|
// TODO: Replace with CSS color() function to calculate alpha when browsers support it
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color() and https://caniuse.com/css-color-function
|
// See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color() and https://caniuse.com/css-color-function
|
||||||
// E6 = 90% alpha
|
// E6 = 90% alpha
|
||||||
--popover-opacity-color-2-mildblack: #222222e6;
|
--floating-menu-opacity-color-2-mildblack: #222222e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|
@ -54,7 +54,7 @@ img {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For placeholder messages (remove eventually)
|
// For placeholder messages (remove eventually)
|
||||||
.popover {
|
.floating-menu {
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiv
|
||||||
import LayoutRow from "../layout/LayoutRow.vue";
|
import LayoutRow from "../layout/LayoutRow.vue";
|
||||||
import LayoutCol from "../layout/LayoutCol.vue";
|
import LayoutCol from "../layout/LayoutCol.vue";
|
||||||
import WorkingColors from "../widgets/WorkingColors.vue";
|
import WorkingColors from "../widgets/WorkingColors.vue";
|
||||||
import { PopoverDirection } from "../widgets/overlays/Popover.vue";
|
import { MenuDirection } from "../widgets/floating-menus/FloatingMenu.vue";
|
||||||
import ShelfItem from "../widgets/ShelfItem.vue";
|
import ShelfItem from "../widgets/ShelfItem.vue";
|
||||||
import Separator, { SeparatorDirection, SeparatorType } from "../widgets/Separator.vue";
|
import Separator, { SeparatorDirection, SeparatorType } from "../widgets/Separator.vue";
|
||||||
import IconButton from "../widgets/buttons/IconButton.vue";
|
import IconButton from "../widgets/buttons/IconButton.vue";
|
||||||
|
|
@ -256,7 +256,7 @@ export default defineComponent({
|
||||||
return {
|
return {
|
||||||
viewportSvg: "",
|
viewportSvg: "",
|
||||||
activeTool: "Select",
|
activeTool: "Select",
|
||||||
PopoverDirection,
|
MenuDirection,
|
||||||
SeparatorDirection,
|
SeparatorDirection,
|
||||||
SeparatorType,
|
SeparatorType,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ import LayoutCol from "../layout/LayoutCol.vue";
|
||||||
import Separator, { SeparatorType } from "../widgets/Separator.vue";
|
import Separator, { SeparatorType } from "../widgets/Separator.vue";
|
||||||
import NumberInput from "../widgets/inputs/NumberInput.vue";
|
import NumberInput from "../widgets/inputs/NumberInput.vue";
|
||||||
import PopoverButton from "../widgets/buttons/PopoverButton.vue";
|
import PopoverButton from "../widgets/buttons/PopoverButton.vue";
|
||||||
import { PopoverDirection } from "../widgets/overlays/Popover.vue";
|
import { MenuDirection } from "../widgets/floating-menus/FloatingMenu.vue";
|
||||||
import IconButton from "../widgets/buttons/IconButton.vue";
|
import IconButton from "../widgets/buttons/IconButton.vue";
|
||||||
import Icon from "../widgets/labels/Icon.vue";
|
import Icon from "../widgets/labels/Icon.vue";
|
||||||
|
|
||||||
|
|
@ -125,7 +125,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
PopoverDirection,
|
MenuDirection,
|
||||||
SeparatorType,
|
SeparatorType,
|
||||||
layers: [] as Array<LayerPanelEntry>,
|
layers: [] as Array<LayerPanelEntry>,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="popover-button">
|
<div class="popover-button">
|
||||||
<IconButton :icon="icon" :size="16" @click="clickButton" />
|
<IconButton :icon="icon" :size="16" @click="clickButton" />
|
||||||
<Popover :direction="PopoverDirection.Bottom" ref="popover">
|
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Bottom" ref="floatingMenu">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</Popover>
|
</FloatingMenu>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
height: 24px;
|
height: 24px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
.popover {
|
.floating-menu {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import IconButton from "./IconButton.vue";
|
import IconButton from "./IconButton.vue";
|
||||||
import Popover, { PopoverDirection } from "../overlays/Popover.vue";
|
import FloatingMenu, { MenuDirection, MenuType } from "../floating-menus/FloatingMenu.vue";
|
||||||
|
|
||||||
export enum PopoverButtonIcon {
|
export enum PopoverButtonIcon {
|
||||||
"DropdownArrow" = "DropdownArrow",
|
"DropdownArrow" = "DropdownArrow",
|
||||||
|
|
@ -50,7 +50,7 @@ export enum PopoverButtonIcon {
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
Popover,
|
FloatingMenu,
|
||||||
IconButton,
|
IconButton,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -58,12 +58,13 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickButton() {
|
clickButton() {
|
||||||
(this.$refs.popover as typeof Popover).setOpen();
|
(this.$refs.floatingMenu as typeof FloatingMenu).setOpen();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
PopoverDirection,
|
MenuDirection,
|
||||||
|
MenuType,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="popover-color-picker">
|
<div class="color-picker">
|
||||||
<div class="saturation-picker" ref="saturationPicker" data-picker-action="MoveSaturation" @pointerdown="onPointerDown">
|
<div class="saturation-picker" ref="saturationPicker" data-picker-action="MoveSaturation" @pointerdown="onPointerDown">
|
||||||
<div ref="saturationCursor" class="selection-circle"></div>
|
<div ref="saturationCursor" class="selection-circle"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.popover-color-picker {
|
.color-picker {
|
||||||
--saturation-picker-hue: #ff0000;
|
--saturation-picker-hue: #ff0000;
|
||||||
--opacity-picker-color: #ff0000;
|
--opacity-picker-color: #ff0000;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -118,7 +118,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import { clamp, hsvToRgb, rgbToHsv, isRGB } from "../../lib/utils";
|
import { clamp, hsvToRgb, rgbToHsv, isRGB } from "../../../lib/utils";
|
||||||
|
|
||||||
const enum ColorPickerState {
|
const enum ColorPickerState {
|
||||||
Idle = "Idle",
|
Idle = "Idle",
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
<template>
|
||||||
|
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open">
|
||||||
|
<div class="tail" v-if="type === MenuType.Popover"></div>
|
||||||
|
<div class="floating-menu-container" ref="floatingMenuContainer">
|
||||||
|
<div class="floating-menu-content" ref="floatingMenuContent">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.floating-menu {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
display: flex;
|
||||||
|
// Floating menus begin at a z-index of 1000
|
||||||
|
z-index: 1000;
|
||||||
|
--floating-menu-content-offset: 0;
|
||||||
|
--floating-menu-content-border-radius: 0 0 4px 4px;
|
||||||
|
|
||||||
|
.tail {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
// Put the tail above the floating menu's shadow
|
||||||
|
z-index: 10;
|
||||||
|
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-menu-container {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.floating-menu-content {
|
||||||
|
background: var(--floating-menu-opacity-color-2-mildblack);
|
||||||
|
box-shadow: var(--color-0-black) 0 0 4px;
|
||||||
|
border-radius: var(--floating-menu-content-border-radius);
|
||||||
|
color: var(--color-e-nearwhite);
|
||||||
|
font-size: inherit;
|
||||||
|
padding: 8px;
|
||||||
|
z-index: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.popover {
|
||||||
|
--floating-menu-content-offset: 10px;
|
||||||
|
--floating-menu-content-border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top,
|
||||||
|
&.bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top .tail {
|
||||||
|
border-width: 8px 6px 0 6px;
|
||||||
|
border-color: var(--floating-menu-opacity-color-2-mildblack) transparent transparent transparent;
|
||||||
|
margin-left: -6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom .tail {
|
||||||
|
border-width: 0 6px 8px 6px;
|
||||||
|
border-color: transparent transparent var(--floating-menu-opacity-color-2-mildblack) transparent;
|
||||||
|
margin-left: -6px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left .tail {
|
||||||
|
border-width: 6px 0 6px 8px;
|
||||||
|
border-color: transparent transparent transparent var(--floating-menu-opacity-color-2-mildblack);
|
||||||
|
margin-top: -6px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right .tail {
|
||||||
|
border-width: 6px 8px 6px 0;
|
||||||
|
border-color: transparent var(--floating-menu-opacity-color-2-mildblack) transparent transparent;
|
||||||
|
margin-top: -6px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top .floating-menu-container {
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: var(--floating-menu-content-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom .floating-menu-container {
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: var(--floating-menu-content-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left .floating-menu-container {
|
||||||
|
align-items: center;
|
||||||
|
margin-right: var(--floating-menu-content-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right .floating-menu-container {
|
||||||
|
align-items: center;
|
||||||
|
margin-left: var(--floating-menu-content-offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
export enum MenuDirection {
|
||||||
|
Top = "Top",
|
||||||
|
Bottom = "Bottom",
|
||||||
|
Left = "Left",
|
||||||
|
Right = "Right",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MenuType {
|
||||||
|
Popover = "Popover",
|
||||||
|
Dropdown = "Dropdown",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {},
|
||||||
|
props: {
|
||||||
|
direction: { type: String, default: MenuDirection.Bottom },
|
||||||
|
type: { type: String, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
open: false,
|
||||||
|
mouseStillDown: false,
|
||||||
|
MenuDirection,
|
||||||
|
MenuType,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
|
||||||
|
const floatingMenuContent = this.$refs.floatingMenuContent as HTMLElement;
|
||||||
|
const workspace = document.querySelector(".workspace-row");
|
||||||
|
|
||||||
|
if (floatingMenuContent && workspace) {
|
||||||
|
const workspaceBounds = workspace.getBoundingClientRect();
|
||||||
|
const floatingMenuBounds = floatingMenuContent.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (this.direction === MenuDirection.Left || this.direction === MenuDirection.Right) {
|
||||||
|
const topOffset = floatingMenuBounds.top - workspaceBounds.top - 8;
|
||||||
|
if (topOffset < 0) floatingMenuContainer.style.transform = `translate(0, ${-topOffset}px)`;
|
||||||
|
|
||||||
|
const bottomOffset = workspaceBounds.bottom - floatingMenuBounds.bottom - 8;
|
||||||
|
if (bottomOffset < 0) floatingMenuContainer.style.transform = `translate(0, ${bottomOffset}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.direction === MenuDirection.Top || this.direction === MenuDirection.Bottom) {
|
||||||
|
const leftOffset = floatingMenuBounds.left - workspaceBounds.left - 8;
|
||||||
|
if (leftOffset < 0) floatingMenuContainer.style.transform = `translate(${-leftOffset}px, 0)`;
|
||||||
|
|
||||||
|
const rightOffset = workspaceBounds.right - floatingMenuBounds.right - 8;
|
||||||
|
if (rightOffset < 0) floatingMenuContainer.style.transform = `translate(${rightOffset}px, 0)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setOpen() {
|
||||||
|
this.open = true;
|
||||||
|
},
|
||||||
|
setClosed() {
|
||||||
|
this.open = false;
|
||||||
|
},
|
||||||
|
mouseMoveHandler(e: MouseEvent) {
|
||||||
|
const MOUSE_STRAY_DISTANCE = 100;
|
||||||
|
|
||||||
|
// Close the floating menu if the mouse has strayed far enough from its bounds
|
||||||
|
if (this.isMouseEventOutsideFloatingMenu(e, MOUSE_STRAY_DISTANCE)) {
|
||||||
|
this.setClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
const eventIncludesLmb = Boolean(e.buttons & 1);
|
||||||
|
|
||||||
|
// Clean up any messes from lost mouseup events
|
||||||
|
if (!this.open && !eventIncludesLmb) {
|
||||||
|
this.mouseStillDown = false;
|
||||||
|
window.removeEventListener("mouseup", this.mouseUpHandler);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mouseDownHandler(e: MouseEvent) {
|
||||||
|
// Close the floating menu if the mouse clicked outside the floating menu (but within stray distance)
|
||||||
|
if (this.isMouseEventOutsideFloatingMenu(e)) {
|
||||||
|
this.setClosed();
|
||||||
|
|
||||||
|
// Track if the left mouse button is now down so its later click event can be canceled
|
||||||
|
const eventIsForLmb = e.button === 0;
|
||||||
|
if (eventIsForLmb) this.mouseStillDown = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mouseUpHandler(e: MouseEvent) {
|
||||||
|
const eventIsForLmb = e.button === 0;
|
||||||
|
|
||||||
|
if (this.mouseStillDown && eventIsForLmb) {
|
||||||
|
// Clean up self
|
||||||
|
this.mouseStillDown = false;
|
||||||
|
window.removeEventListener("mouseup", this.mouseUpHandler);
|
||||||
|
|
||||||
|
// Prevent the click event from firing, which would normally occur right after this mouseup event
|
||||||
|
window.addEventListener("click", this.clickHandlerCapture, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
isMouseEventOutsideFloatingMenu(e: MouseEvent, extraDistanceAllowed = 0): boolean {
|
||||||
|
const floatingMenuContent = this.$refs.floatingMenuContent as HTMLElement;
|
||||||
|
const floatingMenuBounds = floatingMenuContent.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (floatingMenuBounds.left - e.clientX >= extraDistanceAllowed) return true;
|
||||||
|
if (e.clientX - floatingMenuBounds.right >= extraDistanceAllowed) return true;
|
||||||
|
if (floatingMenuBounds.top - e.clientY >= extraDistanceAllowed) return true;
|
||||||
|
if (e.clientY - floatingMenuBounds.bottom >= extraDistanceAllowed) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
open(newState: boolean, oldState: boolean) {
|
||||||
|
if (newState && !oldState) {
|
||||||
|
// Close floating menu if mouse strays far enough away
|
||||||
|
window.addEventListener("mousemove", this.mouseMoveHandler);
|
||||||
|
|
||||||
|
// Close floating menu if mouse is outside (but within stray distance)
|
||||||
|
window.addEventListener("mousedown", this.mouseDownHandler);
|
||||||
|
|
||||||
|
// 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("mouseup", this.mouseUpHandler);
|
||||||
|
}
|
||||||
|
if (!newState && oldState) {
|
||||||
|
window.removeEventListener("mousemove", this.mouseMoveHandler);
|
||||||
|
window.removeEventListener("mousedown", this.mouseDownHandler);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -2,15 +2,15 @@
|
||||||
<div class="swatch-pair">
|
<div class="swatch-pair">
|
||||||
<div class="secondary swatch">
|
<div class="secondary swatch">
|
||||||
<button @click="clickSecondarySwatch" ref="secondaryButton"></button>
|
<button @click="clickSecondarySwatch" ref="secondaryButton"></button>
|
||||||
<Popover :direction="PopoverDirection.Right" horizontal ref="secondarySwatchPopover">
|
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Right" horizontal ref="secondarySwatchFloatingMenu">
|
||||||
<ColorPicker v-model:color="secondaryColor" />
|
<ColorPicker v-model:color="secondaryColor" />
|
||||||
</Popover>
|
</FloatingMenu>
|
||||||
</div>
|
</div>
|
||||||
<div class="primary swatch">
|
<div class="primary swatch">
|
||||||
<button @click="clickPrimarySwatch" ref="primaryButton"></button>
|
<button @click="clickPrimarySwatch" ref="primaryButton"></button>
|
||||||
<Popover :direction="PopoverDirection.Right" horizontal ref="primarySwatchPopover">
|
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Right" horizontal ref="primarySwatchFloatingMenu">
|
||||||
<ColorPicker v-model:color="primaryColor" />
|
<ColorPicker v-model:color="primaryColor" />
|
||||||
</Popover>
|
</FloatingMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover {
|
.floating-menu {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: -2px;
|
right: -2px;
|
||||||
}
|
}
|
||||||
|
|
@ -68,26 +68,26 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { rgbToDecimalRgb } from "@/lib/utils";
|
import { rgbToDecimalRgb } from "@/lib/utils";
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import ColorPicker from "../../popovers/ColorPicker.vue";
|
import ColorPicker from "../floating-menus/ColorPicker.vue";
|
||||||
import Popover, { PopoverDirection } from "../overlays/Popover.vue";
|
import FloatingMenu, { MenuDirection, MenuType } from "../floating-menus/FloatingMenu.vue";
|
||||||
|
|
||||||
const wasm = import("../../../../wasm/pkg");
|
const wasm = import("../../../../wasm/pkg");
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
Popover,
|
FloatingMenu,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
},
|
},
|
||||||
props: {},
|
props: {},
|
||||||
methods: {
|
methods: {
|
||||||
clickPrimarySwatch() {
|
clickPrimarySwatch() {
|
||||||
this.getRef<typeof Popover>("primarySwatchPopover").setOpen();
|
this.getRef<typeof FloatingMenu>("primarySwatchFloatingMenu").setOpen();
|
||||||
this.getRef<typeof Popover>("secondarySwatchPopover").setClosed();
|
this.getRef<typeof FloatingMenu>("secondarySwatchFloatingMenu").setClosed();
|
||||||
},
|
},
|
||||||
|
|
||||||
clickSecondarySwatch() {
|
clickSecondarySwatch() {
|
||||||
this.getRef<typeof Popover>("secondarySwatchPopover").setOpen();
|
this.getRef<typeof FloatingMenu>("secondarySwatchFloatingMenu").setOpen();
|
||||||
this.getRef<typeof Popover>("primarySwatchPopover").setClosed();
|
this.getRef<typeof FloatingMenu>("primarySwatchFloatingMenu").setClosed();
|
||||||
},
|
},
|
||||||
|
|
||||||
getRef<T>(name: string) {
|
getRef<T>(name: string) {
|
||||||
|
|
@ -118,7 +118,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
PopoverDirection,
|
MenuDirection,
|
||||||
|
MenuType,
|
||||||
primaryColor: { r: 0, g: 0, b: 0, a: 1 },
|
primaryColor: { r: 0, g: 0, b: 0, a: 1 },
|
||||||
secondaryColor: { r: 255, g: 255, b: 255, a: 1 },
|
secondaryColor: { r: 255, g: 255, b: 255, a: 1 },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,236 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="popover" :class="direction.toLowerCase()" v-if="open">
|
|
||||||
<div class="tail"></div>
|
|
||||||
<div class="popover-container" ref="popoverContainer">
|
|
||||||
<div class="popover-content" ref="popoverContent">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.popover {
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
display: flex;
|
|
||||||
// Overlays begin at a z-index of 1000
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
&.top,
|
|
||||||
&.bottom {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tail {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-style: solid;
|
|
||||||
// Put the tail above the popover's shadow
|
|
||||||
z-index: 1;
|
|
||||||
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
|
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
.top > & {
|
|
||||||
border-width: 8px 6px 0 6px;
|
|
||||||
border-color: var(--popover-opacity-color-2-mildblack) transparent transparent transparent;
|
|
||||||
margin-left: -6px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom > & {
|
|
||||||
border-width: 0 6px 8px 6px;
|
|
||||||
border-color: transparent transparent var(--popover-opacity-color-2-mildblack) transparent;
|
|
||||||
margin-left: -6px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left > & {
|
|
||||||
border-width: 6px 0 6px 8px;
|
|
||||||
border-color: transparent transparent transparent var(--popover-opacity-color-2-mildblack);
|
|
||||||
margin-top: -6px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right > & {
|
|
||||||
border-width: 6px 8px 6px 0;
|
|
||||||
border-color: transparent var(--popover-opacity-color-2-mildblack) transparent transparent;
|
|
||||||
margin-top: -6px;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-container {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.top > & {
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom > & {
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left > & {
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right > & {
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-content {
|
|
||||||
background: var(--popover-opacity-color-2-mildblack);
|
|
||||||
box-shadow: var(--color-0-black) 0 0 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--color-e-nearwhite);
|
|
||||||
font-size: inherit;
|
|
||||||
padding: 8px;
|
|
||||||
z-index: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export enum PopoverDirection {
|
|
||||||
Top = "Top",
|
|
||||||
Bottom = "Bottom",
|
|
||||||
Left = "Left",
|
|
||||||
Right = "Right",
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {},
|
|
||||||
props: {
|
|
||||||
direction: { type: String, default: PopoverDirection.Bottom },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
open: false,
|
|
||||||
mouseStillDown: false,
|
|
||||||
PopoverDirection,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
updated() {
|
|
||||||
const popoverContainer = this.$refs.popoverContainer as HTMLElement;
|
|
||||||
const popoverContent = this.$refs.popoverContent as HTMLElement;
|
|
||||||
const workspace = document.querySelector(".workspace-row");
|
|
||||||
|
|
||||||
if (popoverContent && workspace) {
|
|
||||||
const workspaceBounds = workspace.getBoundingClientRect();
|
|
||||||
const popoverBounds = popoverContent.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (this.direction === PopoverDirection.Left || this.direction === PopoverDirection.Right) {
|
|
||||||
const topOffset = popoverBounds.top - workspaceBounds.top - 8;
|
|
||||||
if (topOffset < 0) popoverContainer.style.transform = `translate(0, ${-topOffset}px)`;
|
|
||||||
|
|
||||||
const bottomOffset = workspaceBounds.bottom - popoverBounds.bottom - 8;
|
|
||||||
if (bottomOffset < 0) popoverContainer.style.transform = `translate(0, ${bottomOffset}px)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.direction === PopoverDirection.Top || this.direction === PopoverDirection.Bottom) {
|
|
||||||
const leftOffset = popoverBounds.left - workspaceBounds.left - 8;
|
|
||||||
if (leftOffset < 0) popoverContainer.style.transform = `translate(${-leftOffset}px, 0)`;
|
|
||||||
|
|
||||||
const rightOffset = workspaceBounds.right - popoverBounds.right - 8;
|
|
||||||
if (rightOffset < 0) popoverContainer.style.transform = `translate(${rightOffset}px, 0)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setOpen() {
|
|
||||||
this.open = true;
|
|
||||||
},
|
|
||||||
setClosed() {
|
|
||||||
this.open = false;
|
|
||||||
},
|
|
||||||
mouseMoveHandler(e: MouseEvent) {
|
|
||||||
const MOUSE_STRAY_DISTANCE = 100;
|
|
||||||
|
|
||||||
// Close the popover if the mouse has strayed far enough from its bounds
|
|
||||||
if (this.isMouseEventOutsidePopover(e, MOUSE_STRAY_DISTANCE)) {
|
|
||||||
this.setClosed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
const eventIncludesLmb = Boolean(e.buttons & 1);
|
|
||||||
|
|
||||||
// Clean up any messes from lost mouseup events
|
|
||||||
if (!this.open && !eventIncludesLmb) {
|
|
||||||
this.mouseStillDown = false;
|
|
||||||
window.removeEventListener("mouseup", this.mouseUpHandler);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mouseDownHandler(e: MouseEvent) {
|
|
||||||
// Close the popover if the mouse clicked outside the popover (but within stray distance)
|
|
||||||
if (this.isMouseEventOutsidePopover(e)) {
|
|
||||||
this.setClosed();
|
|
||||||
|
|
||||||
// Track if the left mouse button is now down so its later click event can be canceled
|
|
||||||
const eventIsForLmb = e.button === 0;
|
|
||||||
if (eventIsForLmb) this.mouseStillDown = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mouseUpHandler(e: MouseEvent) {
|
|
||||||
const eventIsForLmb = e.button === 0;
|
|
||||||
|
|
||||||
if (this.mouseStillDown && eventIsForLmb) {
|
|
||||||
// Clean up self
|
|
||||||
this.mouseStillDown = false;
|
|
||||||
window.removeEventListener("mouseup", this.mouseUpHandler);
|
|
||||||
|
|
||||||
// Prevent the click event from firing, which would normally occur right after this mouseup event
|
|
||||||
window.addEventListener("click", this.clickHandlerCapture, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clickHandlerCapture(e: MouseEvent) {
|
|
||||||
// Stop the click event from reopening this popover if the click event targets the popover's button
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// Clean up self
|
|
||||||
window.removeEventListener("click", this.clickHandlerCapture, true);
|
|
||||||
},
|
|
||||||
isMouseEventOutsidePopover(e: MouseEvent, extraDistanceAllowed = 0): boolean {
|
|
||||||
const popoverContent = this.$refs.popoverContent as HTMLElement;
|
|
||||||
const popoverBounds = popoverContent.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (popoverBounds.left - e.clientX >= extraDistanceAllowed) return true;
|
|
||||||
if (e.clientX - popoverBounds.right >= extraDistanceAllowed) return true;
|
|
||||||
if (popoverBounds.top - e.clientY >= extraDistanceAllowed) return true;
|
|
||||||
if (e.clientY - popoverBounds.bottom >= extraDistanceAllowed) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
open(newState: boolean, oldState: boolean) {
|
|
||||||
if (newState && !oldState) {
|
|
||||||
// Close popover if mouse strays far enough away
|
|
||||||
window.addEventListener("mousemove", this.mouseMoveHandler);
|
|
||||||
|
|
||||||
// Close popover if mouse is outside (but within stray distance)
|
|
||||||
window.addEventListener("mousedown", this.mouseDownHandler);
|
|
||||||
|
|
||||||
// Cancel the subsequent click event to prevent the popover from reopening if the popover's button is the click event target
|
|
||||||
window.addEventListener("mouseup", this.mouseUpHandler);
|
|
||||||
}
|
|
||||||
if (!newState && oldState) {
|
|
||||||
window.removeEventListener("mousemove", this.mouseMoveHandler);
|
|
||||||
window.removeEventListener("mousedown", this.mouseDownHandler);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -139,7 +139,7 @@ import LayerTree from "../panels/LayerTree.vue";
|
||||||
import Minimap from "../panels/Minimap.vue";
|
import Minimap from "../panels/Minimap.vue";
|
||||||
import IconButton from "../widgets/buttons/IconButton.vue";
|
import IconButton from "../widgets/buttons/IconButton.vue";
|
||||||
import PopoverButton, { PopoverButtonIcon } from "../widgets/buttons/PopoverButton.vue";
|
import PopoverButton, { PopoverButtonIcon } from "../widgets/buttons/PopoverButton.vue";
|
||||||
import { PopoverDirection } from "../widgets/overlays/Popover.vue";
|
import { MenuDirection } from "../widgets/floating-menus/FloatingMenu.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -160,7 +160,7 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
PopoverButtonIcon,
|
PopoverButtonIcon,
|
||||||
PopoverDirection,
|
MenuDirection,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue