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
@@ -80,9 +80,9 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import IconLabel, { IconName } from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
data() {
@@ -96,9 +96,9 @@ export default defineComponent({
},
},
props: {
checked: { type: Boolean, required: true },
icon: { type: String, default: "Checkmark" },
outlineStyle: { type: Boolean, default: false },
checked: { type: Boolean as PropType<boolean>, required: true },
icon: { type: String as PropType<IconName>, default: "Checkmark" },
outlineStyle: { type: Boolean as PropType<boolean>, default: false },
},
components: { IconLabel },
});
@@ -1,16 +1,16 @@
<template>
<div class="dropdown-input">
<div class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px`, disabled: 'disabled' }" @click="clickDropdownBox" data-hover-menu-spawner>
<div class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px`, disabled: 'disabled' }" @click="() => clickDropdownBox()" data-hover-menu-spawner>
<IconLabel :class="'dropdown-icon'" :icon="activeEntry.icon" v-if="activeEntry.icon" />
<span>{{ activeEntry.label }}</span>
<IconLabel :class="'dropdown-arrow'" :icon="'DropdownArrow'" />
</div>
<MenuList
v-model:active-entry="activeEntry"
@update:activeEntry="activeEntryChanged"
@width-changed="onWidthChanged"
v-model:activeEntry="activeEntry"
@update:activeEntry="(newActiveEntry) => activeEntryChanged(newActiveEntry)"
@widthChanged="(newWidth) => onWidthChanged(newWidth)"
:menuEntries="menuEntries"
:direction="MenuDirection.Bottom"
:direction="'Bottom'"
:drawIcon="drawIcon"
:scrollable="true"
ref="menuList"
@@ -90,21 +90,19 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import MenuList, { MenuListEntry, SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
import { MenuDirection } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
props: {
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
selectedIndex: { type: Number, required: true },
drawIcon: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
selectedIndex: { type: Number as PropType<number>, required: true },
drawIcon: { type: Boolean as PropType<boolean>, default: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
},
data() {
return {
activeEntry: this.menuEntries.flat()[this.selectedIndex],
MenuDirection,
minWidth: 0,
};
},
@@ -10,7 +10,7 @@
<IconLabel :icon="entry.icon" v-if="entry.icon" />
<span v-if="entry.label">{{ entry.label }}</span>
</div>
<MenuList :menuEntries="entry.children" :direction="MenuDirection.Bottom" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref) => setEntryRefs(entry, ref)" />
<MenuList :menuEntries="entry.children" :direction="'Bottom'" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref) => setEntryRefs(entry, ref)" />
</div>
</div>
</template>
@@ -53,12 +53,11 @@
<script lang="ts">
import { defineComponent } from "vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import { ApplicationPlatform } from "@/components/window/MainWindow.vue";
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
import { MenuDirection } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import { EditorState } from "@/state/wasm-loader";
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
function makeMenuEntries(editor: EditorState): MenuListEntries {
return [
{
@@ -66,8 +65,8 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
ref: undefined,
children: [
[
{ label: "New", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: async () => editor.instance.new_document() },
{ label: "Open…", shortcut: ["KeyControl", "KeyO"], action: async () => editor.instance.open_document() },
{ label: "New", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: async (): Promise<void> => editor.instance.new_document() },
{ label: "Open…", shortcut: ["KeyControl", "KeyO"], action: async (): Promise<void> => editor.instance.open_document() },
{
label: "Open Recent",
shortcut: ["KeyControl", "KeyShift", "KeyO"],
@@ -84,18 +83,18 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
},
],
[
{ label: "Close", shortcut: ["KeyControl", "KeyW"], shortcutRequiresLock: true, action: async () => editor.instance.close_active_document_with_confirmation() },
{ label: "Close All", shortcut: ["KeyControl", "KeyAlt", "KeyW"], action: async () => editor.instance.close_all_documents_with_confirmation() },
{ label: "Close", shortcut: ["KeyControl", "KeyW"], shortcutRequiresLock: true, action: async (): Promise<void> => editor.instance.close_active_document_with_confirmation() },
{ label: "Close All", shortcut: ["KeyControl", "KeyAlt", "KeyW"], action: async (): Promise<void> => editor.instance.close_all_documents_with_confirmation() },
],
[
{ label: "Save", shortcut: ["KeyControl", "KeyS"], action: async () => editor.instance.save_document() },
{ label: "Save As…", shortcut: ["KeyControl", "KeyShift", "KeyS"], action: async () => editor.instance.save_document() },
{ label: "Save", shortcut: ["KeyControl", "KeyS"], action: async (): Promise<void> => editor.instance.save_document() },
{ label: "Save As…", shortcut: ["KeyControl", "KeyShift", "KeyS"], action: async (): Promise<void> => editor.instance.save_document() },
{ label: "Save All", shortcut: ["KeyControl", "KeyAlt", "KeyS"] },
{ label: "Auto-Save", checkbox: true, checked: true },
],
[
{ label: "Import…", shortcut: ["KeyControl", "KeyI"] },
{ label: "Export…", shortcut: ["KeyControl", "KeyE"], action: async () => editor.instance.export_document() },
{ label: "Export…", shortcut: ["KeyControl", "KeyE"], action: async (): Promise<void> => editor.instance.export_document() },
],
[{ label: "Quit", shortcut: ["KeyControl", "KeyQ"] }],
],
@@ -105,13 +104,13 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
ref: undefined,
children: [
[
{ label: "Undo", shortcut: ["KeyControl", "KeyZ"], action: async () => editor.instance.undo() },
{ label: "Redo", shortcut: ["KeyControl", "KeyShift", "KeyZ"], action: async () => editor.instance.redo() },
{ label: "Undo", shortcut: ["KeyControl", "KeyZ"], action: async (): Promise<void> => editor.instance.undo() },
{ label: "Redo", shortcut: ["KeyControl", "KeyShift", "KeyZ"], action: async (): Promise<void> => editor.instance.redo() },
],
[
{ label: "Cut", shortcut: ["KeyControl", "KeyX"], action: async () => editor.instance.cut() },
{ label: "Copy", icon: "Copy", shortcut: ["KeyControl", "KeyC"], action: async () => editor.instance.copy() },
{ label: "Paste", icon: "Paste", shortcut: ["KeyControl", "KeyV"], action: async () => editor.instance.paste() },
{ label: "Cut", shortcut: ["KeyControl", "KeyX"], action: async (): Promise<void> => editor.instance.cut() },
{ label: "Copy", icon: "Copy", shortcut: ["KeyControl", "KeyC"], action: async (): Promise<void> => editor.instance.copy() },
{ label: "Paste", icon: "Paste", shortcut: ["KeyControl", "KeyV"], action: async (): Promise<void> => editor.instance.paste() },
],
],
},
@@ -120,8 +119,8 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
ref: undefined,
children: [
[
{ label: "Select All", shortcut: ["KeyControl", "KeyA"], action: async () => editor.instance.select_all_layers() },
{ label: "Deselect All", shortcut: ["KeyControl", "KeyAlt", "KeyA"], action: async () => editor.instance.deselect_all_layers() },
{ label: "Select All", shortcut: ["KeyControl", "KeyA"], action: async (): Promise<void> => editor.instance.select_all_layers() },
{ label: "Deselect All", shortcut: ["KeyControl", "KeyAlt", "KeyA"], action: async (): Promise<void> => editor.instance.deselect_all_layers() },
{
label: "Order",
children: [
@@ -129,14 +128,14 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
{
label: "Raise To Front",
shortcut: ["KeyControl", "KeyShift", "KeyLeftBracket"],
action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_max()),
action: async (): Promise<void> => editor.instance.reorder_selected_layers(editor.rawWasm.i32_max()),
},
{ label: "Raise", shortcut: ["KeyControl", "KeyRightBracket"], action: async () => editor.instance.reorder_selected_layers(1) },
{ label: "Lower", shortcut: ["KeyControl", "KeyLeftBracket"], action: async () => editor.instance.reorder_selected_layers(-1) },
{ label: "Raise", shortcut: ["KeyControl", "KeyRightBracket"], action: async (): Promise<void> => editor.instance.reorder_selected_layers(1) },
{ label: "Lower", shortcut: ["KeyControl", "KeyLeftBracket"], action: async (): Promise<void> => editor.instance.reorder_selected_layers(-1) },
{
label: "Lower to Back",
shortcut: ["KeyControl", "KeyShift", "KeyRightBracket"],
action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_min()),
action: async (): Promise<void> => editor.instance.reorder_selected_layers(editor.rawWasm.i32_min()),
},
],
],
@@ -158,12 +157,12 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
label: "Help",
ref: undefined,
children: [
[{ label: "About Graphite", action: async () => editor.instance.request_about_graphite_dialog() }],
[{ label: "About Graphite", action: async (): Promise<void> => editor.instance.request_about_graphite_dialog() }],
[
{ label: "Report a Bug", action: () => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
{ label: "Visit on GitHub", action: () => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
{ label: "Report a Bug", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
{ label: "Visit on GitHub", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
],
[{ label: "Debug: Panic (DANGER)", action: async () => editor.rawWasm.intentional_panic() }],
[{ label: "Debug: Panic (DANGER)", action: async (): Promise<void> => editor.rawWasm.intentional_panic() }],
],
},
];
@@ -186,10 +185,8 @@ export default defineComponent({
},
data() {
return {
ApplicationPlatform,
menuEntries: makeMenuEntries(this.editor),
MenuDirection,
comingSoon: () => this.dialog.comingSoon(),
comingSoon: (): void => this.dialog.comingSoon(),
};
},
components: {
@@ -12,8 +12,8 @@
:disabled="disabled"
/>
<label v-if="label" :for="`number-input-${id}`">{{ label }}</label>
<button v-if="!Number.isNaN(value)" class="arrow left" @click="onIncrement(IncrementDirection.Decrease)"></button>
<button v-if="!Number.isNaN(value)" class="arrow right" @click="onIncrement(IncrementDirection.Increase)"></button>
<button v-if="!Number.isNaN(value)" class="arrow left" @click="onIncrement('Decrease')"></button>
<button v-if="!Number.isNaN(value)" class="arrow right" @click="onIncrement('Increase')"></button>
</div>
</template>
@@ -152,40 +152,29 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
export enum IncrementBehavior {
Add = "Add",
Multiply = "Multiply",
Callback = "Callback",
None = "None",
}
export enum IncrementDirection {
Decrease = "Decrease",
Increase = "Increase",
}
export type IncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
export type IncrementDirection = "Decrease" | "Increase";
export default defineComponent({
components: {},
props: {
value: { type: Number, required: true },
min: { type: Number, required: false },
max: { type: Number, required: false },
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: IncrementBehavior.Add },
incrementFactor: { type: Number, default: 1 },
incrementCallbackIncrease: { type: Function, required: false },
incrementCallbackDecrease: { type: Function, required: false },
isInteger: { type: Boolean, default: false },
unit: { type: String, default: "" },
unitIsHiddenWhenEditing: { type: Boolean, default: true },
displayDecimalPlaces: { type: Number, default: 3 },
label: { type: String, required: false },
disabled: { type: Boolean, default: false },
value: { type: Number as PropType<number>, required: true },
min: { type: Number as PropType<number>, required: false },
max: { type: Number as PropType<number>, required: false },
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
incrementFactor: { type: Number as PropType<number>, default: 1 },
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
incrementCallbackDecrease: { type: Function as PropType<() => void>, required: false },
isInteger: { type: Boolean as PropType<boolean>, default: false },
unit: { type: String as PropType<string>, default: "" },
unitIsHiddenWhenEditing: { type: Boolean as PropType<boolean>, default: true },
displayDecimalPlaces: { type: Number as PropType<number>, default: 3 },
label: { type: String as PropType<string>, required: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
},
data() {
return {
text: `${this.value}${this.unit}`,
editing: false,
IncrementDirection,
id: `${Math.random()}`.substring(2),
};
},
@@ -225,19 +214,19 @@ export default defineComponent({
if (Number.isNaN(this.value)) return;
switch (this.incrementBehavior) {
case IncrementBehavior.Add: {
const directionAddend = direction === IncrementDirection.Increase ? this.incrementFactor : -this.incrementFactor;
case "Add": {
const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor;
this.updateValue(this.value + directionAddend);
break;
}
case IncrementBehavior.Multiply: {
const directionMultiplier = direction === IncrementDirection.Increase ? this.incrementFactor : 1 / this.incrementFactor;
case "Multiply": {
const directionMultiplier = direction === "Increase" ? this.incrementFactor : 1 / this.incrementFactor;
this.updateValue(this.value * directionMultiplier);
break;
}
case IncrementBehavior.Callback: {
if (direction === IncrementDirection.Increase && this.incrementCallbackIncrease) this.incrementCallbackIncrease();
if (direction === IncrementDirection.Decrease && this.incrementCallbackDecrease) this.incrementCallbackDecrease();
case "Callback": {
if (direction === "Increase" && this.incrementCallbackIncrease) this.incrementCallbackIncrease();
if (direction === "Decrease" && this.incrementCallbackDecrease) this.incrementCallbackDecrease();
break;
}
default:
@@ -34,14 +34,15 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
import { IconName } from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
props: {
checked: { type: Boolean, required: true },
icon: { type: String, default: "Checkmark" },
checked: { type: Boolean as PropType<boolean>, required: true },
icon: { type: String as PropType<IconName>, default: "Checkmark" },
},
components: {
CheckboxInput,
@@ -85,7 +85,7 @@ export type RadioEntries = RadioEntryData[];
export default defineComponent({
props: {
entries: { type: Array as PropType<RadioEntries>, required: true },
selectedIndex: { type: Number, required: true },
selectedIndex: { type: Number as PropType<number>, required: true },
},
methods: {
handleEntryClick(menuEntry: RadioEntryData) {
@@ -29,16 +29,17 @@
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, PropType } from "vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import { IconName } from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
components: { IconButton },
props: {
icon: { type: String, required: true },
action: { type: Function, required: true },
active: { type: Boolean, default: false },
icon: { type: String as PropType<IconName>, required: true },
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
active: { type: Boolean as PropType<boolean>, default: false },
},
});
</script>
@@ -1,15 +1,15 @@
<template>
<div class="swatch-pair">
<div class="secondary swatch">
<button @click="clickSecondarySwatch" ref="secondaryButton" data-hover-menu-spawner></button>
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Right" horizontal ref="secondarySwatchFloatingMenu">
<ColorPicker @update:color="secondaryColorChanged" :color="secondaryColor" />
<button @click="() => clickSecondarySwatch()" ref="secondaryButton" data-hover-menu-spawner></button>
<FloatingMenu :type="'Popover'" :direction="'Right'" horizontal ref="secondarySwatchFloatingMenu">
<ColorPicker @update:color="(color) => secondaryColorChanged(color)" :color="secondaryColor" />
</FloatingMenu>
</div>
<div class="primary swatch">
<button @click="clickPrimarySwatch" ref="primaryButton" data-hover-menu-spawner></button>
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Right" horizontal ref="primarySwatchFloatingMenu">
<ColorPicker @update:color="primaryColorChanged" :color="primaryColor" />
<button @click="() => clickPrimarySwatch()" ref="primaryButton" data-hover-menu-spawner></button>
<FloatingMenu :type="'Popover'" :direction="'Right'" horizontal ref="primarySwatchFloatingMenu">
<ColorPicker @update:color="(color) => primaryColorChanged(color)" :color="primaryColor" />
</FloatingMenu>
</div>
</div>
@@ -68,11 +68,11 @@
<script lang="ts">
import { defineComponent } from "vue";
import { rgbToDecimalRgb, RGB } from "@/utilities/color";
import { RGBA, UpdateWorkingColors } from "@/dispatcher/js-messages";
import { rgbaToDecimalRgba } from "@/utilities/color";
import ColorPicker from "@/components/widgets/floating-menus/ColorPicker.vue";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import { UpdateWorkingColors } from "@/dispatcher/js-messages";
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
export default defineComponent({
inject: ["editor"],
@@ -80,70 +80,56 @@ export default defineComponent({
FloatingMenu,
ColorPicker,
},
props: {},
methods: {
clickPrimarySwatch() {
this.getRef<typeof FloatingMenu>("primarySwatchFloatingMenu").setOpen();
this.getRef<typeof FloatingMenu>("secondarySwatchFloatingMenu").setClosed();
(this.$refs.primarySwatchFloatingMenu as typeof FloatingMenu).setOpen();
(this.$refs.secondarySwatchFloatingMenu as typeof FloatingMenu).setClosed();
},
clickSecondarySwatch() {
this.getRef<typeof FloatingMenu>("secondarySwatchFloatingMenu").setOpen();
this.getRef<typeof FloatingMenu>("primarySwatchFloatingMenu").setClosed();
(this.$refs.secondarySwatchFloatingMenu as typeof FloatingMenu).setOpen();
(this.$refs.primarySwatchFloatingMenu as typeof FloatingMenu).setClosed();
},
getRef<T>(name: string) {
return this.$refs[name] as T;
},
primaryColorChanged(color: RGB) {
primaryColorChanged(color: RGBA) {
this.primaryColor = color;
this.updatePrimaryColor();
},
secondaryColorChanged(color: RGB) {
secondaryColorChanged(color: RGBA) {
this.secondaryColor = color;
this.updateSecondaryColor();
},
async updatePrimaryColor() {
let color = this.primaryColor;
const button = this.getRef<HTMLButtonElement>("primaryButton");
const button = this.$refs.primaryButton as HTMLButtonElement;
button.style.setProperty("--swatch-color", `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);
color = rgbToDecimalRgb(this.primaryColor);
color = rgbaToDecimalRgba(this.primaryColor);
this.editor.instance.update_primary_color(color.r, color.g, color.b, color.a);
},
async updateSecondaryColor() {
let color = this.secondaryColor;
const button = this.getRef<HTMLButtonElement>("secondaryButton");
const button = this.$refs.secondaryButton as HTMLButtonElement;
button.style.setProperty("--swatch-color", `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);
color = rgbToDecimalRgb(this.secondaryColor);
color = rgbaToDecimalRgba(this.secondaryColor);
this.editor.instance.update_secondary_color(color.r, color.g, color.b, color.a);
},
},
data() {
return {
MenuDirection,
MenuType,
primaryColor: { r: 0, g: 0, b: 0, a: 1 },
secondaryColor: { r: 255, g: 255, b: 255, a: 1 },
primaryColor: { r: 0, g: 0, b: 0, a: 1 } as RGBA,
secondaryColor: { r: 255, g: 255, b: 255, a: 1 } as RGBA,
};
},
mounted() {
this.editor.dispatcher.subscribeJsMessage(UpdateWorkingColors, (updateWorkingColors) => {
const { primary, secondary } = updateWorkingColors;
this.primaryColor = updateWorkingColors.primary.toRgba();
this.secondaryColor = updateWorkingColors.secondary.toRgba();
this.primaryColor = primary.toRgba();
this.secondaryColor = secondary.toRgba();
const primaryButton = this.$refs.primaryButton as HTMLButtonElement;
primaryButton.style.setProperty("--swatch-color", updateWorkingColors.primary.toRgbaCSS());
const primaryButton = this.getRef<HTMLButtonElement>("primaryButton");
primaryButton.style.setProperty("--swatch-color", primary.toRgbaCSS());
const secondaryButton = this.getRef<HTMLButtonElement>("secondaryButton");
secondaryButton.style.setProperty("--swatch-color", secondary.toRgbaCSS());
const secondaryButton = this.$refs.secondaryButton as HTMLButtonElement;
secondaryButton.style.setProperty("--swatch-color", updateWorkingColors.secondary.toRgbaCSS());
});
this.updatePrimaryColor();