Major frontend code cleanup (#452)

Many large changes, including:
- TypeScript enums are now string unions throughout
- Strong type-checking throughout the TS and Vue codebase
- Vue component props now all specify `as PropType<...>`
- Usage of annotated return types on all functions
- Sorting of JS import statements
- Explicit usage of Vue bind attribute function call arguments (`@click="foo"` is now `@click=(e) => foo(e)`)
- Much improved code quality related to the color picker
- Consistent camelCase Vue bind and v-model attributes
- Consistent Vue HTML attribute strings with single quotes
- Bug fix and clarity improvement with incorrect hint class parameters
- Empty Vue component objects like `props: {}` and `components: {}` removed
This commit is contained in:
Keavon Chambers
2022-01-02 06:00:02 -08:00
parent 1954aceb0e
commit 7e0cbb60b4
53 changed files with 842 additions and 946 deletions
@@ -1,12 +1,12 @@
<template>
<div class="color-picker">
<div class="saturation-picker" ref="saturationPicker" data-picker-action="MoveSaturation" @pointerdown="onPointerDown">
<div class="saturation-picker" ref="saturationPicker" @pointerdown="(e) => onPointerDown(e)">
<div ref="saturationCursor" class="selection-circle"></div>
</div>
<div class="hue-picker" ref="huePicker" data-picker-action="MoveHue" @pointerdown="onPointerDown">
<div class="hue-picker" ref="huePicker" @pointerdown="(e) => onPointerDown(e)">
<div ref="hueCursor" class="selection-pincers"></div>
</div>
<div class="opacity-picker" ref="opacityPicker" data-picker-action="MoveOpacity" @pointerdown="onPointerDown">
<div class="opacity-picker" ref="opacityPicker" @pointerdown="(e) => onPointerDown(e)">
<div ref="opacityCursor" class="selection-pincers"></div>
</div>
</div>
@@ -117,43 +117,28 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import { hsvToRgb, rgbToHsv, isRGB } from "@/utilities/color";
import { RGBA } from "@/dispatcher/js-messages";
import { hsvaToRgba, rgbaToHsva } from "@/utilities/color";
import { clamp } from "@/utilities/math";
const enum ColorPickerState {
Idle = "Idle",
MoveHue = "MoveHue",
MoveOpacity = "MoveOpacity",
MoveSaturation = "MoveSaturation",
}
type ColorPickerState = "Idle" | "MoveHue" | "MoveOpacity" | "MoveSaturation";
// TODO: Clean up the fundamental code design in this file to simplify it and use better practices.
// TODO: Such as removing the `picker*` data variables and reducing the number of functions which call each other in weird, non-obvious ways.
export default defineComponent({
components: {},
props: {
color: { type: Object, required: true },
color: { type: Object as PropType<RGBA>, required: true },
},
data() {
return {
state: ColorPickerState.Idle,
// Disable proxy on this object
// https://v3.vuejs.org/api/options-data.html#data-2
// eslint-disable-next-line vue/no-reserved-keys
_: {
colorPicker: {
color: { h: 0, s: 0, v: 0, a: 1 },
hue: {
rect: { width: 0, height: 0, top: 0, left: 0 },
},
opacity: {
rect: { width: 0, height: 0, top: 0, left: 0 },
},
saturation: {
rect: { width: 0, height: 0, top: 0, left: 0 },
},
},
},
state: "Idle" as ColorPickerState,
pickerHSVA: { h: 0, s: 0, v: 0, a: 1 },
pickerHueRect: { width: 0, height: 0, top: 0, left: 0 },
pickerOpacityRect: { width: 0, height: 0, top: 0, left: 0 },
pickerSaturationRect: { width: 0, height: 0, top: 0, left: 0 },
};
},
mounted() {
@@ -171,116 +156,122 @@ export default defineComponent({
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
},
getRef<T>(name: string) {
return this.$refs[name] as T;
},
onPointerDown(e: PointerEvent) {
if (!(e.currentTarget instanceof Element)) return;
const picker = e.currentTarget.getAttribute("data-picker-action");
this.state = (() => {
switch (picker) {
case "MoveHue":
return ColorPickerState.MoveHue;
case "MoveOpacity":
return ColorPickerState.MoveOpacity;
case "MoveSaturation":
return ColorPickerState.MoveSaturation;
default:
return ColorPickerState.Idle;
}
})();
if (!(e.currentTarget instanceof HTMLElement)) return;
if (this.state !== ColorPickerState.Idle) {
this.addEvents();
this.updateRects();
this.onPointerMove(e);
if ((this.$refs.saturationPicker as HTMLElement).contains(e.currentTarget)) {
this.state = "MoveSaturation";
} else if ((this.$refs.huePicker as HTMLElement).contains(e.currentTarget)) {
this.state = "MoveHue";
} else if ((this.$refs.opacityPicker as HTMLElement).contains(e.currentTarget)) {
this.state = "MoveOpacity";
} else {
this.state = "Idle";
}
if (this.state === "Idle") return;
this.addEvents();
this.updateRects();
this.onPointerMove(e);
},
onPointerMove(e: PointerEvent) {
const { colorPicker } = this.$data._;
if (this.state === ColorPickerState.MoveHue) {
this.setHuePosition(e.clientY - colorPicker.hue.rect.top);
} else if (this.state === ColorPickerState.MoveOpacity) {
this.setOpacityPosition(e.clientY - colorPicker.opacity.rect.top);
} else if (this.state === ColorPickerState.MoveSaturation) {
this.setSaturationPosition(e.clientX - colorPicker.saturation.rect.left, e.clientY - colorPicker.saturation.rect.top);
switch (this.state) {
case "MoveHue":
this.setHueCursorPosition(e.clientY - this.pickerHueRect.top);
break;
case "MoveOpacity":
this.setOpacityCursorPosition(e.clientY - this.pickerOpacityRect.top);
break;
case "MoveSaturation":
this.setSaturationCursorPosition(e.clientX - this.pickerSaturationRect.left, e.clientY - this.pickerSaturationRect.top);
break;
default:
return;
}
if (this.state !== ColorPickerState.Idle) {
this.updateHue();
this.$emit("update:color", hsvToRgb(colorPicker.color));
}
this.updateHue();
// The `color` prop's watcher calls `this.updateColor()`
this.$emit("update:color", hsvaToRgba(this.pickerHSVA));
},
onPointerUp() {
if (this.state !== ColorPickerState.Idle) {
this.state = ColorPickerState.Idle;
this.removeEvents();
}
if (this.state === "Idle") return;
this.state = "Idle";
this.removeEvents();
},
updateRects() {
const { colorPicker } = this.$data._;
const saturationPicker = this.getRef<HTMLElement>("saturationPicker");
// Saturation
const saturationPicker = this.$refs.saturationPicker as HTMLElement;
const saturation = saturationPicker.getBoundingClientRect();
colorPicker.saturation.rect.width = saturation.width;
colorPicker.saturation.rect.height = saturation.height;
colorPicker.saturation.rect.left = saturation.left;
colorPicker.saturation.rect.top = saturation.top;
const huePicker = this.getRef<HTMLElement>("huePicker");
this.pickerSaturationRect.width = saturation.width;
this.pickerSaturationRect.height = saturation.height;
this.pickerSaturationRect.left = saturation.left;
this.pickerSaturationRect.top = saturation.top;
// Hue
const huePicker = this.$refs.huePicker as HTMLElement;
const hue = huePicker.getBoundingClientRect();
colorPicker.hue.rect.width = hue.width;
colorPicker.hue.rect.height = hue.height;
colorPicker.hue.rect.left = hue.left;
colorPicker.hue.rect.top = hue.top;
const opacityPicker = this.getRef<HTMLElement>("opacityPicker");
this.pickerHueRect.width = hue.width;
this.pickerHueRect.height = hue.height;
this.pickerHueRect.left = hue.left;
this.pickerHueRect.top = hue.top;
// Opacity
const opacityPicker = this.$refs.opacityPicker as HTMLElement;
const opacity = opacityPicker.getBoundingClientRect();
colorPicker.opacity.rect.width = opacity.width;
colorPicker.opacity.rect.height = opacity.height;
colorPicker.opacity.rect.left = opacity.left;
colorPicker.opacity.rect.top = opacity.top;
this.pickerOpacityRect.width = opacity.width;
this.pickerOpacityRect.height = opacity.height;
this.pickerOpacityRect.left = opacity.left;
this.pickerOpacityRect.top = opacity.top;
},
setSaturationPosition(x: number, y: number) {
const { colorPicker } = this.$data._;
const saturationCursor = this.getRef<HTMLElement>("saturationCursor");
const saturationPosition = [clamp(x, 0, colorPicker.saturation.rect.width), clamp(y, 0, colorPicker.saturation.rect.height)];
saturationCursor.style.transform = `translate(${saturationPosition[0]}px, ${saturationPosition[1]}px)`;
colorPicker.color.s = saturationPosition[0] / colorPicker.saturation.rect.width;
colorPicker.color.v = (1 - saturationPosition[1] / colorPicker.saturation.rect.height) * 255;
setSaturationCursorPosition(x: number, y: number) {
const saturationPositionX = clamp(x, 0, this.pickerSaturationRect.width);
const saturationPositionY = clamp(y, 0, this.pickerSaturationRect.height);
const saturationCursor = this.$refs.saturationCursor as HTMLElement;
saturationCursor.style.transform = `translate(${saturationPositionX}px, ${saturationPositionY}px)`;
this.pickerHSVA.s = saturationPositionX / this.pickerSaturationRect.width;
this.pickerHSVA.v = (1 - saturationPositionY / this.pickerSaturationRect.height) * 255;
},
setHuePosition(y: number) {
const { colorPicker } = this.$data._;
const hueCursor = this.getRef<HTMLElement>("hueCursor");
const huePosition = clamp(y, 0, colorPicker.hue.rect.height);
setHueCursorPosition(y: number) {
const huePosition = clamp(y, 0, this.pickerHueRect.height);
const hueCursor = this.$refs.hueCursor as HTMLElement;
hueCursor.style.transform = `translateY(${huePosition}px)`;
colorPicker.color.h = clamp(1 - huePosition / colorPicker.hue.rect.height);
this.pickerHSVA.h = clamp(1 - huePosition / this.pickerHueRect.height);
},
setOpacityPosition(y: number) {
const { colorPicker } = this.$data._;
const opacityCursor = this.getRef<HTMLElement>("opacityCursor");
const opacityPosition = clamp(y, 0, colorPicker.opacity.rect.height);
setOpacityCursorPosition(y: number) {
const opacityPosition = clamp(y, 0, this.pickerOpacityRect.height);
const opacityCursor = this.$refs.opacityCursor as HTMLElement;
opacityCursor.style.transform = `translateY(${opacityPosition}px)`;
colorPicker.color.a = clamp(1 - opacityPosition / colorPicker.opacity.rect.height);
this.pickerHSVA.a = clamp(1 - opacityPosition / this.pickerOpacityRect.height);
},
updateHue() {
const { colorPicker } = this.$data._;
let color = hsvToRgb({ h: colorPicker.color.h, s: 1, v: 255, a: 1 });
this.$el.style.setProperty("--saturation-picker-hue", `rgb(${color.r}, ${color.g}, ${color.b})`);
color = hsvToRgb(colorPicker.color);
this.$el.style.setProperty("--opacity-picker-color", `rgb(${color.r}, ${color.g}, ${color.b})`);
const hsva = hsvaToRgba({ h: this.pickerHSVA.h, s: 1, v: 255, a: 1 });
const rgba = hsvaToRgba(this.pickerHSVA);
this.$el.style.setProperty("--saturation-picker-hue", `rgb(${hsva.r}, ${hsva.g}, ${hsva.b})`);
this.$el.style.setProperty("--opacity-picker-color", `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})`);
},
updateColor() {
if (this.state !== ColorPickerState.Idle) return;
const { color } = this;
if (!isRGB(color)) return;
const { colorPicker } = this.$data._;
colorPicker.color = rgbToHsv(color);
if (this.state !== "Idle") return;
this.pickerHSVA = rgbaToHsva(this.color);
this.updateRects();
this.setSaturationPosition(colorPicker.color.s * colorPicker.saturation.rect.width, (1 - colorPicker.color.v / 255) * colorPicker.saturation.rect.height);
this.setOpacityPosition((1 - colorPicker.color.a) * colorPicker.opacity.rect.height);
this.setHuePosition((1 - colorPicker.color.h) * colorPicker.hue.rect.height);
this.setSaturationCursorPosition(this.pickerHSVA.s * this.pickerSaturationRect.width, (1 - this.pickerHSVA.v / 255) * this.pickerSaturationRect.height);
this.setOpacityCursorPosition((1 - this.pickerHSVA.a) * this.pickerOpacityRect.height);
this.setHueCursorPosition((1 - this.pickerHSVA.h) * this.pickerHueRect.height);
this.updateHue();
},
},
@@ -1,6 +1,6 @@
<template>
<div class="dialog-modal">
<FloatingMenu :type="MenuType.Dialog" :direction="MenuDirection.Center">
<FloatingMenu :type="'Dialog'" :direction="'Center'">
<LayoutRow>
<LayoutCol :class="'icon-column'">
<!-- `dialog.state.icon` class exists to provide special sizing in CSS to specific icons -->
@@ -79,12 +79,12 @@
<script lang="ts">
import { defineComponent } from "vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
export default defineComponent({
inject: ["dialog"],
@@ -101,11 +101,5 @@ export default defineComponent({
this.dialog.dismissDialog();
},
},
data() {
return {
MenuDirection,
MenuType,
};
},
});
</script>
@@ -1,6 +1,6 @@
<template>
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open || type === MenuType.Dialog" ref="floatingMenu">
<div class="tail" v-if="type === MenuType.Popover"></div>
<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">
<slot></slot>
@@ -177,36 +177,20 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
export enum MenuDirection {
Top = "Top",
Bottom = "Bottom",
Left = "Left",
Right = "Right",
TopLeft = "TopLeft",
TopRight = "TopRight",
BottomLeft = "BottomLeft",
BottomRight = "BottomRight",
Center = "Center",
}
export enum MenuType {
Popover = "Popover",
Dropdown = "Dropdown",
Dialog = "Dialog",
}
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center";
export type MenuType = "Popover" | "Dropdown" | "Dialog";
const POINTER_STRAY_DISTANCE = 100;
export default defineComponent({
components: {},
props: {
direction: { type: String, default: MenuDirection.Bottom },
type: { type: String, required: true },
windowEdgeMargin: { type: Number, default: 6 },
minWidth: { type: Number, default: 0 },
scrollable: { type: Boolean, default: false },
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
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 },
},
data() {
const containerResizeObserver = new ResizeObserver((entries) => {
@@ -218,8 +202,6 @@ export default defineComponent({
open: false,
pointerStillDown: false,
containerResizeObserver,
MenuDirection,
MenuType,
};
},
updated() {
@@ -235,8 +217,8 @@ export default defineComponent({
let zeroedBorderDirection1: Edge | undefined;
let zeroedBorderDirection2: Edge | undefined;
if (this.direction === MenuDirection.Top || this.direction === MenuDirection.Bottom) {
zeroedBorderDirection1 = this.direction === MenuDirection.Top ? "Bottom" : "Top";
if (this.direction === "Top" || this.direction === "Bottom") {
zeroedBorderDirection1 = this.direction === "Top" ? "Bottom" : "Top";
if (floatingMenuBounds.left - this.windowEdgeMargin <= workspaceBounds.left) {
floatingMenuContent.style.left = `${this.windowEdgeMargin}px`;
@@ -249,8 +231,8 @@ export default defineComponent({
}
}
if (this.direction === MenuDirection.Left || this.direction === MenuDirection.Right) {
zeroedBorderDirection2 = this.direction === MenuDirection.Left ? "Right" : "Left";
if (this.direction === "Left" || this.direction === "Right") {
zeroedBorderDirection2 = this.direction === "Left" ? "Right" : "Left";
if (floatingMenuBounds.top - this.windowEdgeMargin <= workspaceBounds.top) {
floatingMenuContent.style.top = `${this.windowEdgeMargin}px`;
@@ -264,7 +246,7 @@ export default defineComponent({
}
// Remove the rounded corner from where the tail perfectly meets the corner
if (this.type === MenuType.Popover && this.windowEdgeMargin === 6 && zeroedBorderDirection1 && zeroedBorderDirection2) {
if (this.type === "Popover" && this.windowEdgeMargin === 6 && zeroedBorderDirection1 && zeroedBorderDirection2) {
switch (`${zeroedBorderDirection1}${zeroedBorderDirection2}`) {
case "TopLeft":
floatingMenuContent.style.borderTopLeftRadius = "0";
@@ -1,7 +1,7 @@
<template>
<FloatingMenu :class="'menu-list'" :direction="direction" :type="MenuType.Dropdown" ref="floatingMenu" :windowEdgeMargin="0" :scrollable="scrollable" data-hover-menu-keep-open>
<FloatingMenu :class="'menu-list'" :direction="direction" :type="'Dropdown'" ref="floatingMenu" :windowEdgeMargin="0" :scrollable="scrollable" data-hover-menu-keep-open>
<template v-for="(section, sectionIndex) in menuEntries" :key="sectionIndex">
<Separator :type="SeparatorType.List" :direction="SeparatorDirection.Vertical" v-if="sectionIndex > 0" />
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
<div
v-for="(entry, entryIndex) in section"
:key="entryIndex"
@@ -26,7 +26,7 @@
<MenuList
v-if="entry.children"
:direction="MenuDirection.TopRight"
:direction="'TopRight'"
:menuEntries="entry.children"
v-bind="{ defaultAction, minWidth, drawIcon, scrollable }"
:ref="(ref) => setEntryRefs(entry, ref)"
@@ -132,13 +132,11 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import FloatingMenu, { MenuDirection } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
export type MenuListEntries<Value = string> = MenuListEntry<Value>[];
export type SectionsOfMenuListEntries<Value = string> = MenuListEntries<Value>[];
@@ -162,13 +160,13 @@ const KEYBOARD_LOCK_SWITCH_BROWSER = "This hotkey is reserved by the browser, bu
const MenuList = defineComponent({
inject: ["fullscreen"],
props: {
direction: { type: String as PropType<MenuDirection>, default: MenuDirection.Bottom },
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
activeEntry: { type: Object as PropType<MenuListEntry>, required: false },
defaultAction: { type: Function as PropType<() => void | undefined>, required: false },
minWidth: { type: Number, default: 0 },
drawIcon: { type: Boolean, default: false },
scrollable: { type: Boolean, default: false },
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 },
},
methods: {
setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu) {
@@ -231,7 +229,7 @@ const MenuList = defineComponent({
// Restore open/closed state if it was forced open for measurement
if (!initiallyOpen) floatingMenu.setClosed();
this.$emit("width-changed", width);
this.$emit("widthChanged", width);
});
});
},
@@ -265,10 +263,6 @@ const MenuList = defineComponent({
data() {
return {
keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
SeparatorDirection,
SeparatorType,
MenuDirection,
MenuType,
};
},
components: {