Move JS script to the top of each Vue file

This commit is contained in:
Keavon Chambers
2023-03-10 03:44:49 -08:00
parent 1c50f0030c
commit 2327524690
50 changed files with 3482 additions and 3517 deletions
@@ -1,3 +1,268 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { clamp } from "@/utility-functions/math";
import type { HSV, RGB } from "@/wasm-communication/messages";
import { Color } from "@/wasm-communication/messages";
import FloatingMenu, { type MenuDirection } from "@/components/layout/FloatingMenu.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import TextInput from "@/components/widgets/inputs/TextInput.vue";
import Separator from "@/components/widgets/labels/Separator.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
type PresetColors = "none" | "black" | "white" | "red" | "yellow" | "green" | "cyan" | "blue" | "magenta";
const PURE_COLORS: Record<PresetColors, [number, number, number]> = {
none: [0, 0, 0],
black: [0, 0, 0],
white: [1, 1, 1],
red: [1, 0, 0],
yellow: [1, 1, 0],
green: [0, 1, 0],
cyan: [0, 1, 1],
blue: [0, 0, 1],
magenta: [1, 0, 1],
};
const COLOR_SPACE_CHOICES = [[{ label: "sRGB" }]];
export default defineComponent({
inject: ["editor"],
emits: ["update:color", "update:open"],
props: {
color: { type: Object as PropType<Color>, required: true },
allowNone: { type: Boolean as PropType<boolean>, default: false },
allowTransparency: { type: Boolean as PropType<boolean>, default: false }, // TODO: Implement this
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
// TODO: See if this should be made to follow the pattern of DropdownInput.vue so this could be removed
open: { type: Boolean as PropType<boolean>, required: true },
},
data() {
const hsvaOrNone = this.color.toHSVA();
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 };
return {
hue: hsva.h,
saturation: hsva.s,
value: hsva.v,
alpha: hsva.a,
isNone: hsvaOrNone === undefined,
initialHue: hsva.h,
initialSaturation: hsva.s,
initialValue: hsva.v,
initialAlpha: hsva.a,
initialIsNone: hsvaOrNone === undefined,
draggingPickerTrack: undefined as HTMLDivElement | undefined,
colorSpaceChoices: COLOR_SPACE_CHOICES,
strayCloses: true,
};
},
computed: {
opaqueHueColor(): Color {
return new Color({ h: this.hue, s: 1, v: 1, a: 1 });
},
newColor(): Color {
if (this.isNone) return new Color("none");
return new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
},
initialColor(): Color {
if (this.initialIsNone) return new Color("none");
return new Color({ h: this.initialHue, s: this.initialSaturation, v: this.initialValue, a: this.initialAlpha });
},
black(): Color {
return new Color(0, 0, 0, 1);
},
},
watch: {
// Called only when `open` is changed from outside this component (with v-model)
open(isOpen: boolean) {
if (isOpen) this.setInitialHSVA(this.hue, this.saturation, this.value, this.alpha, this.isNone);
},
// Called only when `color` is changed from outside this component (with v-model)
color(color: Color) {
const hsva = color.toHSVA();
if (hsva !== undefined) {
// Update the hue, but only if it is necessary so we don't:
// - ...jump the user's hue from 360° (top) to the equivalent 0° (bottom)
// - ...reset the hue to 0° if the color is fully desaturated, where all hues are equivalent
// - ...reset the hue to 0° if the color's value is black, where all hues are equivalent
if (!(hsva.h === 0 && this.hue === 1) && hsva.s > 0 && hsva.v > 0) this.hue = hsva.h;
// Update the saturation, but only if it is necessary so we don't:
// - ...reset the saturation to the left is the color's value is black along the bottom edge, where all saturations are equivalent
if (hsva.v !== 0) this.saturation = hsva.s;
// Update the value
this.value = hsva.v;
// Update the alpha
this.alpha = hsva.a;
// Update the status of this not being a color
this.isNone = false;
} else {
this.setNewHSVA(0, 0, 0, 1, true);
}
},
},
methods: {
onPointerDown(e: PointerEvent) {
const target = (e.target || undefined) as HTMLElement | undefined;
this.draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
this.addEvents();
this.onPointerMove(e);
},
onPointerMove(e: PointerEvent) {
// Just in case the mouseup event is lost
if (e.buttons === 0) this.removeEvents();
if (this.draggingPickerTrack?.hasAttribute("data-saturation-value-picker")) {
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
this.saturation = clamp((e.clientX - rectangle.left) / rectangle.width, 0, 1);
this.value = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
this.strayCloses = false;
} else if (this.draggingPickerTrack?.hasAttribute("data-hue-picker")) {
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
this.hue = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
this.strayCloses = false;
} else if (this.draggingPickerTrack?.hasAttribute("data-alpha-picker")) {
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
this.alpha = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
this.strayCloses = false;
}
const color = new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
this.setColor(color);
},
onPointerUp() {
this.removeEvents();
},
addEvents() {
document.addEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointerup", this.onPointerUp);
},
removeEvents() {
this.draggingPickerTrack = undefined;
this.strayCloses = true;
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
},
emitOpenState(isOpen: boolean) {
this.$emit("update:open", isOpen);
},
setColor(color?: Color) {
const colorToEmit = color || new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
this.$emit("update:color", colorToEmit);
},
swapNewWithInitial() {
const initial = this.initialColor;
const tempHue = this.hue;
const tempSaturation = this.saturation;
const tempValue = this.value;
const tempAlpha = this.alpha;
const tempIsNone = this.isNone;
this.setNewHSVA(this.initialHue, this.initialSaturation, this.initialValue, this.initialAlpha, this.initialIsNone);
this.setInitialHSVA(tempHue, tempSaturation, tempValue, tempAlpha, tempIsNone);
this.setColor(initial);
},
setColorCode(colorCode: string) {
const color = Color.fromCSS(colorCode);
if (color) this.setColor(color);
},
setColorRGB(channel: keyof RGB, strength: number) {
if (channel === "r") this.setColor(new Color(strength / 255, this.newColor.green, this.newColor.blue, this.newColor.alpha));
else if (channel === "g") this.setColor(new Color(this.newColor.red, strength / 255, this.newColor.blue, this.newColor.alpha));
else if (channel === "b") this.setColor(new Color(this.newColor.red, this.newColor.green, strength / 255, this.newColor.alpha));
},
setColorHSV(channel: keyof HSV, strength: number) {
if (channel === "h") this.hue = strength / 360;
else if (channel === "s") this.saturation = strength / 100;
else if (channel === "v") this.value = strength / 100;
this.setColor();
},
setColorAlphaPercent(alpha: number) {
this.alpha = alpha / 100;
this.setColor();
},
setColorPresetSubtile(e: MouseEvent) {
const clickedTile = e.target as HTMLDivElement | undefined;
const tileColor = clickedTile?.getAttribute("data-pure-tile") || undefined;
if (tileColor) this.setColorPreset(tileColor as PresetColors);
},
setColorPreset(preset: PresetColors) {
if (preset === "none") {
this.setNewHSVA(0, 0, 0, 1, true);
this.setColor(new Color("none"));
return;
}
const presetColor = new Color(...PURE_COLORS[preset], 1);
const hsva = presetColor.toHSVA() || { h: 0, s: 0, v: 0, a: 0 };
this.setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, false);
this.setColor(presetColor);
},
setNewHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) {
this.hue = hue;
this.saturation = saturation;
this.value = value;
this.alpha = alpha;
this.isNone = isNone;
},
setInitialHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) {
this.initialHue = hue;
this.initialSaturation = saturation;
this.initialValue = value;
this.initialAlpha = alpha;
this.initialIsNone = isNone;
},
async activateEyedropperSample() {
// TODO: Replace this temporary solution that only works in Chromium-based browsers with the custom color sampler used by the Eyedropper tool
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!(window as any).EyeDropper) {
this.editor.instance.eyedropperSampleForColorPicker();
return;
}
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await new (window as any).EyeDropper().open();
this.setColorCode(result.sRGBHex);
} catch {
// Do nothing
}
},
},
unmounted() {
this.removeEvents();
},
components: {
DropdownInput,
FloatingMenu,
IconButton,
LayoutCol,
LayoutRow,
NumberInput,
Separator,
TextInput,
TextLabel,
},
});
</script>
<template>
<FloatingMenu class="color-picker" :open="open" @update:open="(isOpen) => emitOpenState(isOpen)" :strayCloses="strayCloses" :direction="direction" :type="'Popover'">
<LayoutRow
@@ -354,268 +619,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { clamp } from "@/utility-functions/math";
import type { HSV, RGB } from "@/wasm-communication/messages";
import { Color } from "@/wasm-communication/messages";
import FloatingMenu, { type MenuDirection } from "@/components/layout/FloatingMenu.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import TextInput from "@/components/widgets/inputs/TextInput.vue";
import Separator from "@/components/widgets/labels/Separator.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
type PresetColors = "none" | "black" | "white" | "red" | "yellow" | "green" | "cyan" | "blue" | "magenta";
const PURE_COLORS: Record<PresetColors, [number, number, number]> = {
none: [0, 0, 0],
black: [0, 0, 0],
white: [1, 1, 1],
red: [1, 0, 0],
yellow: [1, 1, 0],
green: [0, 1, 0],
cyan: [0, 1, 1],
blue: [0, 0, 1],
magenta: [1, 0, 1],
};
const COLOR_SPACE_CHOICES = [[{ label: "sRGB" }]];
export default defineComponent({
inject: ["editor"],
emits: ["update:color", "update:open"],
props: {
color: { type: Object as PropType<Color>, required: true },
allowNone: { type: Boolean as PropType<boolean>, default: false },
allowTransparency: { type: Boolean as PropType<boolean>, default: false }, // TODO: Implement this
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
// TODO: See if this should be made to follow the pattern of DropdownInput.vue so this could be removed
open: { type: Boolean as PropType<boolean>, required: true },
},
data() {
const hsvaOrNone = this.color.toHSVA();
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 };
return {
hue: hsva.h,
saturation: hsva.s,
value: hsva.v,
alpha: hsva.a,
isNone: hsvaOrNone === undefined,
initialHue: hsva.h,
initialSaturation: hsva.s,
initialValue: hsva.v,
initialAlpha: hsva.a,
initialIsNone: hsvaOrNone === undefined,
draggingPickerTrack: undefined as HTMLDivElement | undefined,
colorSpaceChoices: COLOR_SPACE_CHOICES,
strayCloses: true,
};
},
computed: {
opaqueHueColor(): Color {
return new Color({ h: this.hue, s: 1, v: 1, a: 1 });
},
newColor(): Color {
if (this.isNone) return new Color("none");
return new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
},
initialColor(): Color {
if (this.initialIsNone) return new Color("none");
return new Color({ h: this.initialHue, s: this.initialSaturation, v: this.initialValue, a: this.initialAlpha });
},
black(): Color {
return new Color(0, 0, 0, 1);
},
},
watch: {
// Called only when `open` is changed from outside this component (with v-model)
open(isOpen: boolean) {
if (isOpen) this.setInitialHSVA(this.hue, this.saturation, this.value, this.alpha, this.isNone);
},
// Called only when `color` is changed from outside this component (with v-model)
color(color: Color) {
const hsva = color.toHSVA();
if (hsva !== undefined) {
// Update the hue, but only if it is necessary so we don't:
// - ...jump the user's hue from 360° (top) to the equivalent 0° (bottom)
// - ...reset the hue to 0° if the color is fully desaturated, where all hues are equivalent
// - ...reset the hue to 0° if the color's value is black, where all hues are equivalent
if (!(hsva.h === 0 && this.hue === 1) && hsva.s > 0 && hsva.v > 0) this.hue = hsva.h;
// Update the saturation, but only if it is necessary so we don't:
// - ...reset the saturation to the left is the color's value is black along the bottom edge, where all saturations are equivalent
if (hsva.v !== 0) this.saturation = hsva.s;
// Update the value
this.value = hsva.v;
// Update the alpha
this.alpha = hsva.a;
// Update the status of this not being a color
this.isNone = false;
} else {
this.setNewHSVA(0, 0, 0, 1, true);
}
},
},
methods: {
onPointerDown(e: PointerEvent) {
const target = (e.target || undefined) as HTMLElement | undefined;
this.draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
this.addEvents();
this.onPointerMove(e);
},
onPointerMove(e: PointerEvent) {
// Just in case the mouseup event is lost
if (e.buttons === 0) this.removeEvents();
if (this.draggingPickerTrack?.hasAttribute("data-saturation-value-picker")) {
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
this.saturation = clamp((e.clientX - rectangle.left) / rectangle.width, 0, 1);
this.value = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
this.strayCloses = false;
} else if (this.draggingPickerTrack?.hasAttribute("data-hue-picker")) {
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
this.hue = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
this.strayCloses = false;
} else if (this.draggingPickerTrack?.hasAttribute("data-alpha-picker")) {
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
this.alpha = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
this.strayCloses = false;
}
const color = new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
this.setColor(color);
},
onPointerUp() {
this.removeEvents();
},
addEvents() {
document.addEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointerup", this.onPointerUp);
},
removeEvents() {
this.draggingPickerTrack = undefined;
this.strayCloses = true;
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
},
emitOpenState(isOpen: boolean) {
this.$emit("update:open", isOpen);
},
setColor(color?: Color) {
const colorToEmit = color || new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
this.$emit("update:color", colorToEmit);
},
swapNewWithInitial() {
const initial = this.initialColor;
const tempHue = this.hue;
const tempSaturation = this.saturation;
const tempValue = this.value;
const tempAlpha = this.alpha;
const tempIsNone = this.isNone;
this.setNewHSVA(this.initialHue, this.initialSaturation, this.initialValue, this.initialAlpha, this.initialIsNone);
this.setInitialHSVA(tempHue, tempSaturation, tempValue, tempAlpha, tempIsNone);
this.setColor(initial);
},
setColorCode(colorCode: string) {
const color = Color.fromCSS(colorCode);
if (color) this.setColor(color);
},
setColorRGB(channel: keyof RGB, strength: number) {
if (channel === "r") this.setColor(new Color(strength / 255, this.newColor.green, this.newColor.blue, this.newColor.alpha));
else if (channel === "g") this.setColor(new Color(this.newColor.red, strength / 255, this.newColor.blue, this.newColor.alpha));
else if (channel === "b") this.setColor(new Color(this.newColor.red, this.newColor.green, strength / 255, this.newColor.alpha));
},
setColorHSV(channel: keyof HSV, strength: number) {
if (channel === "h") this.hue = strength / 360;
else if (channel === "s") this.saturation = strength / 100;
else if (channel === "v") this.value = strength / 100;
this.setColor();
},
setColorAlphaPercent(alpha: number) {
this.alpha = alpha / 100;
this.setColor();
},
setColorPresetSubtile(e: MouseEvent) {
const clickedTile = e.target as HTMLDivElement | undefined;
const tileColor = clickedTile?.getAttribute("data-pure-tile") || undefined;
if (tileColor) this.setColorPreset(tileColor as PresetColors);
},
setColorPreset(preset: PresetColors) {
if (preset === "none") {
this.setNewHSVA(0, 0, 0, 1, true);
this.setColor(new Color("none"));
return;
}
const presetColor = new Color(...PURE_COLORS[preset], 1);
const hsva = presetColor.toHSVA() || { h: 0, s: 0, v: 0, a: 0 };
this.setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, false);
this.setColor(presetColor);
},
setNewHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) {
this.hue = hue;
this.saturation = saturation;
this.value = value;
this.alpha = alpha;
this.isNone = isNone;
},
setInitialHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) {
this.initialHue = hue;
this.initialSaturation = saturation;
this.initialValue = value;
this.initialAlpha = alpha;
this.initialIsNone = isNone;
},
async activateEyedropperSample() {
// TODO: Replace this temporary solution that only works in Chromium-based browsers with the custom color sampler used by the Eyedropper tool
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!(window as any).EyeDropper) {
this.editor.instance.eyedropperSampleForColorPicker();
return;
}
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await new (window as any).EyeDropper().open();
this.setColorCode(result.sRGBHex);
} catch {
// Do nothing
}
},
},
unmounted() {
this.removeEvents();
},
components: {
DropdownInput,
FloatingMenu,
IconButton,
LayoutCol,
LayoutRow,
NumberInput,
Separator,
TextInput,
TextLabel,
},
});
</script>
@@ -1,3 +1,37 @@
<script lang="ts">
import { defineComponent } from "vue";
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
export default defineComponent({
inject: ["dialog"],
methods: {
dismiss() {
this.dialog.dismissDialog();
},
},
mounted() {
// Focus the first button in the popup
const dialogModal: HTMLDivElement | undefined = this.$el;
const emphasizedOrFirstButton = (dialogModal?.querySelector("[data-emphasized]") || dialogModal?.querySelector("[data-text-button]") || undefined) as HTMLButtonElement | undefined;
emphasizedOrFirstButton?.focus();
},
components: {
FloatingMenu,
IconLabel,
LayoutCol,
LayoutRow,
TextButton,
WidgetLayout,
},
});
</script>
<template>
<FloatingMenu :open="true" class="dialog-modal" :type="'Dialog'" :direction="'Center'" data-dialog-modal>
<LayoutRow>
@@ -65,37 +99,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent } from "vue";
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
export default defineComponent({
inject: ["dialog"],
methods: {
dismiss() {
this.dialog.dismissDialog();
},
},
mounted() {
// Focus the first button in the popup
const dialogModal: HTMLDivElement | undefined = this.$el;
const emphasizedOrFirstButton = (dialogModal?.querySelector("[data-emphasized]") || dialogModal?.querySelector("[data-text-button]") || undefined) as HTMLButtonElement | undefined;
emphasizedOrFirstButton?.focus();
},
components: {
FloatingMenu,
IconLabel,
LayoutCol,
LayoutRow,
TextButton,
WidgetLayout,
},
});
</script>
@@ -1,3 +1,61 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
// Should be equal to the width and height of the canvas in the CSS above
const ZOOM_WINDOW_DIMENSIONS_EXPANDED = 110;
// SHould be equal to the width and height of the `.pixel-outline` div in the CSS above, and should be evenly divisible into the number above
const UPSCALE_FACTOR = 10;
export const ZOOM_WINDOW_DIMENSIONS = ZOOM_WINDOW_DIMENSIONS_EXPANDED / UPSCALE_FACTOR;
const temporaryCanvas = document.createElement("canvas");
export default defineComponent({
props: {
imageData: { type: Object as PropType<ImageData> },
colorChoice: { type: String as PropType<string>, required: true },
primaryColor: { type: String as PropType<string>, required: true },
secondaryColor: { type: String as PropType<string>, required: true },
},
mounted() {
this.displayImageDataPreview(this.imageData);
},
watch: {
imageData(imageData: ImageData | undefined) {
this.displayImageDataPreview(imageData);
},
},
methods: {
displayImageDataPreview(imageData: ImageData | undefined) {
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");
temporaryCanvas.width = ZOOM_WINDOW_DIMENSIONS;
temporaryCanvas.height = ZOOM_WINDOW_DIMENSIONS;
const temporaryContext = temporaryCanvas.getContext("2d");
if (!imageData || !context || !temporaryContext) return;
temporaryContext.putImageData(imageData, 0, 0, 0, 0, ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS);
context.fillStyle = "black";
context.fillRect(0, 0, ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS);
context.drawImage(temporaryCanvas, 0, 0);
},
},
components: {
FloatingMenu,
},
});
</script>
<template>
<FloatingMenu
:open="true"
@@ -79,61 +137,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
// Should be equal to the width and height of the canvas in the CSS above
const ZOOM_WINDOW_DIMENSIONS_EXPANDED = 110;
// SHould be equal to the width and height of the `.pixel-outline` div in the CSS above, and should be evenly divisible into the number above
const UPSCALE_FACTOR = 10;
export const ZOOM_WINDOW_DIMENSIONS = ZOOM_WINDOW_DIMENSIONS_EXPANDED / UPSCALE_FACTOR;
const temporaryCanvas = document.createElement("canvas");
export default defineComponent({
props: {
imageData: { type: Object as PropType<ImageData> },
colorChoice: { type: String as PropType<string>, required: true },
primaryColor: { type: String as PropType<string>, required: true },
secondaryColor: { type: String as PropType<string>, required: true },
},
mounted() {
this.displayImageDataPreview(this.imageData);
},
watch: {
imageData(imageData: ImageData | undefined) {
this.displayImageDataPreview(imageData);
},
},
methods: {
displayImageDataPreview(imageData: ImageData | undefined) {
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");
temporaryCanvas.width = ZOOM_WINDOW_DIMENSIONS;
temporaryCanvas.height = ZOOM_WINDOW_DIMENSIONS;
const temporaryContext = temporaryCanvas.getContext("2d");
if (!imageData || !context || !temporaryContext) return;
temporaryContext.putImageData(imageData, 0, 0, 0, 0, ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS);
context.fillStyle = "black";
context.fillRect(0, 0, ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS);
context.drawImage(temporaryCanvas, 0, 0);
},
},
components: {
FloatingMenu,
},
});
</script>
@@ -1,166 +1,3 @@
<template>
<FloatingMenu
class="menu-list"
v-model:open="isOpen"
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
:type="'Dropdown'"
:windowEdgeMargin="0"
:escapeCloses="false"
v-bind="{ direction, scrollableY: scrollableY && virtualScrollingEntryHeight === 0, minWidth }"
ref="floatingMenu"
>
<!-- If we put the scrollableY on the layoutcol for non-font dropdowns then for some reason it always creates a tiny scrollbar.
However when we are using the virtual scrolling then we need the layoutcol to be scrolling so we can bind the events without using $refs. -->
<LayoutCol ref="scroller" :scrollableY="scrollableY && virtualScrollingEntryHeight !== 0" @scroll="onScroll" :style="{ minWidth: virtualScrollingEntryHeight ? `${minWidth}px` : `inherit` }">
<LayoutRow v-if="virtualScrollingEntryHeight" class="scroll-spacer" :style="{ height: `${virtualScrollingStartIndex * virtualScrollingEntryHeight}px` }"></LayoutRow>
<template v-for="(section, sectionIndex) in entries" :key="sectionIndex">
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
<LayoutRow
v-for="(entry, entryIndex) in virtualScrollingEntryHeight ? section.slice(virtualScrollingStartIndex, virtualScrollingEndIndex) : section"
:key="entryIndex + (virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0)"
class="row"
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: entry.disabled }"
:style="{ height: virtualScrollingEntryHeight || '20px' }"
:title="tooltip"
@click="() => !entry.disabled && onEntryClick(entry)"
@pointerenter="() => !entry.disabled && onEntryPointerEnter(entry)"
@pointerleave="() => !entry.disabled && onEntryPointerLeave(entry)"
>
<IconLabel v-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
<div v-else-if="drawIcon" class="no-icon"></div>
<link v-if="entry.font" rel="stylesheet" :href="entry.font?.toString()" />
<TextLabel class="entry-label" :style="{ fontFamily: `${!entry.font ? 'inherit' : entry.value}` }">{{ entry.label }}</TextLabel>
<UserInputLabel v-if="entry.shortcut?.keys.length" :keysWithLabelsGroups="[entry.shortcut.keys]" :requiresLock="entry.shortcutRequiresLock" />
<div class="submenu-arrow" v-if="entry.children?.length"></div>
<div class="no-submenu-arrow" v-else></div>
<MenuList
v-if="entry.children"
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
:open="entry.ref?.open || false"
:direction="'TopRight'"
:entries="entry.children"
v-bind="{ minWidth, drawIcon, scrollableY }"
:ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
/>
</LayoutRow>
</template>
<LayoutRow
v-if="virtualScrollingEntryHeight"
class="scroll-spacer"
:style="{ height: `${virtualScrollingTotalHeight - virtualScrollingEndIndex * virtualScrollingEntryHeight}px` }"
></LayoutRow>
</LayoutCol>
</FloatingMenu>
</template>
<style lang="scss">
.menu-list {
.floating-menu-container .floating-menu-content {
padding: 4px 0;
.separator div {
background: var(--color-4-dimgray);
}
.scroll-spacer {
flex: 0 0 auto;
}
.row {
height: 20px;
align-items: center;
white-space: nowrap;
position: relative;
flex: 0 0 auto;
& > * {
flex: 0 0 auto;
}
.entry-icon svg {
fill: var(--color-e-nearwhite);
}
.no-icon {
width: 16px;
}
.entry-label {
flex: 1 1 100%;
margin-left: 8px;
}
.entry-icon,
.no-icon {
margin: 0 4px;
& + .entry-label {
margin-left: 0;
}
}
.user-input-label {
margin-left: 16px;
}
.submenu-arrow {
width: 0;
height: 0;
border-style: solid;
border-width: 3px 0 3px 6px;
border-color: transparent transparent transparent var(--color-e-nearwhite);
}
.no-submenu-arrow {
width: 6px;
}
.submenu-arrow,
.no-submenu-arrow {
margin-left: 6px;
margin-right: 4px;
}
&:hover,
&.open {
background: var(--color-6-lowergray);
color: var(--color-f-white);
.entry-icon svg {
fill: var(--color-f-white);
}
}
&.active {
background: var(--color-e-nearwhite);
color: var(--color-2-mildblack);
.entry-icon svg {
fill: var(--color-2-mildblack);
}
}
&.disabled {
color: var(--color-8-uppergray);
&:hover {
background: none;
}
svg {
fill: var(--color-8-uppergray);
}
}
}
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
@@ -357,3 +194,166 @@ const MenuList = defineComponent({
});
export default MenuList;
</script>
<template>
<FloatingMenu
class="menu-list"
v-model:open="isOpen"
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
:type="'Dropdown'"
:windowEdgeMargin="0"
:escapeCloses="false"
v-bind="{ direction, scrollableY: scrollableY && virtualScrollingEntryHeight === 0, minWidth }"
ref="floatingMenu"
>
<!-- If we put the scrollableY on the layoutcol for non-font dropdowns then for some reason it always creates a tiny scrollbar.
However when we are using the virtual scrolling then we need the layoutcol to be scrolling so we can bind the events without using $refs. -->
<LayoutCol ref="scroller" :scrollableY="scrollableY && virtualScrollingEntryHeight !== 0" @scroll="onScroll" :style="{ minWidth: virtualScrollingEntryHeight ? `${minWidth}px` : `inherit` }">
<LayoutRow v-if="virtualScrollingEntryHeight" class="scroll-spacer" :style="{ height: `${virtualScrollingStartIndex * virtualScrollingEntryHeight}px` }"></LayoutRow>
<template v-for="(section, sectionIndex) in entries" :key="sectionIndex">
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
<LayoutRow
v-for="(entry, entryIndex) in virtualScrollingEntryHeight ? section.slice(virtualScrollingStartIndex, virtualScrollingEndIndex) : section"
:key="entryIndex + (virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0)"
class="row"
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: entry.disabled }"
:style="{ height: virtualScrollingEntryHeight || '20px' }"
:title="tooltip"
@click="() => !entry.disabled && onEntryClick(entry)"
@pointerenter="() => !entry.disabled && onEntryPointerEnter(entry)"
@pointerleave="() => !entry.disabled && onEntryPointerLeave(entry)"
>
<IconLabel v-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
<div v-else-if="drawIcon" class="no-icon"></div>
<link v-if="entry.font" rel="stylesheet" :href="entry.font?.toString()" />
<TextLabel class="entry-label" :style="{ fontFamily: `${!entry.font ? 'inherit' : entry.value}` }">{{ entry.label }}</TextLabel>
<UserInputLabel v-if="entry.shortcut?.keys.length" :keysWithLabelsGroups="[entry.shortcut.keys]" :requiresLock="entry.shortcutRequiresLock" />
<div class="submenu-arrow" v-if="entry.children?.length"></div>
<div class="no-submenu-arrow" v-else></div>
<MenuList
v-if="entry.children"
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
:open="entry.ref?.open || false"
:direction="'TopRight'"
:entries="entry.children"
v-bind="{ minWidth, drawIcon, scrollableY }"
:ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
/>
</LayoutRow>
</template>
<LayoutRow
v-if="virtualScrollingEntryHeight"
class="scroll-spacer"
:style="{ height: `${virtualScrollingTotalHeight - virtualScrollingEndIndex * virtualScrollingEntryHeight}px` }"
></LayoutRow>
</LayoutCol>
</FloatingMenu>
</template>
<style lang="scss">
.menu-list {
.floating-menu-container .floating-menu-content {
padding: 4px 0;
.separator div {
background: var(--color-4-dimgray);
}
.scroll-spacer {
flex: 0 0 auto;
}
.row {
height: 20px;
align-items: center;
white-space: nowrap;
position: relative;
flex: 0 0 auto;
& > * {
flex: 0 0 auto;
}
.entry-icon svg {
fill: var(--color-e-nearwhite);
}
.no-icon {
width: 16px;
}
.entry-label {
flex: 1 1 100%;
margin-left: 8px;
}
.entry-icon,
.no-icon {
margin: 0 4px;
& + .entry-label {
margin-left: 0;
}
}
.user-input-label {
margin-left: 16px;
}
.submenu-arrow {
width: 0;
height: 0;
border-style: solid;
border-width: 3px 0 3px 6px;
border-color: transparent transparent transparent var(--color-e-nearwhite);
}
.no-submenu-arrow {
width: 6px;
}
.submenu-arrow,
.no-submenu-arrow {
margin-left: 6px;
margin-right: 4px;
}
&:hover,
&.open {
background: var(--color-6-lowergray);
color: var(--color-f-white);
.entry-icon svg {
fill: var(--color-f-white);
}
}
&.active {
background: var(--color-e-nearwhite);
color: var(--color-2-mildblack);
.entry-icon svg {
fill: var(--color-2-mildblack);
}
}
&.disabled {
color: var(--color-8-uppergray);
&:hover {
background: none;
}
svg {
fill: var(--color-8-uppergray);
}
}
}
}
}
</style>