Tidy up the full frontend codebase and use optional chaining where possible (#620)

* Tidy up the full frontend codebase and use optional chaining where possible

* Code review changes
This commit is contained in:
Keavon Chambers 2022-04-30 02:52:53 -07:00
parent 92ee3bbad3
commit 07736a9fca
28 changed files with 640 additions and 622 deletions

View file

@ -318,11 +318,8 @@ export default defineComponent({
this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.documents, this.fullscreen);
},
beforeUnmount() {
const { inputManager } = this;
if (inputManager) inputManager.removeListeners();
const { editor } = this;
editor.instance.free();
this.inputManager?.removeListeners();
this.editor.instance.free();
},
components: {
MainWindow,

View file

@ -329,22 +329,21 @@ export default defineComponent({
const rulerHorizontal = this.$refs.rulerHorizontal as typeof CanvasRuler;
const rulerVertical = this.$refs.rulerVertical as typeof CanvasRuler;
if (rulerHorizontal) rulerHorizontal.handleResize();
if (rulerVertical) rulerVertical.handleResize();
rulerHorizontal?.handleResize();
rulerVertical?.handleResize();
},
pasteFile(e: DragEvent) {
const { dataTransfer } = e;
if (!dataTransfer) return;
e.preventDefault();
Array.from(dataTransfer.items).forEach((item) => {
Array.from(dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile();
if (file && file.type.startsWith("image")) {
file.arrayBuffer().then((buffer): void => {
const u8Array = new Uint8Array(buffer);
if (file?.type.startsWith("image")) {
const buffer = await file.arrayBuffer();
const u8Array = new Uint8Array(buffer);
this.editor.instance.paste_image(file.type, u8Array, e.clientX, e.clientY);
});
this.editor.instance.paste_image(file.type, u8Array, e.clientX, e.clientY);
}
});
},
@ -400,11 +399,13 @@ export default defineComponent({
const range = document.createRange();
range.selectNodeContents(addedInput);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
addedInput.focus();
addedInput.click();
});
@ -455,24 +456,20 @@ export default defineComponent({
this.canvasCursor = updateMouseCursor.cursor;
});
this.editor.dispatcher.subscribeJsMessage(TriggerTextCommit, () => {
if (this.textInput) this.editor.instance.on_change_text(textInputCleanup(this.textInput.innerText));
if (this.textInput) {
const textCleaned = textInputCleanup(this.textInput.innerText);
this.editor.instance.on_change_text(textCleaned);
}
});
this.editor.dispatcher.subscribeJsMessage(TriggerFontLoad, (triggerFontLoad) => {
fetch(triggerFontLoad.font)
.then((response) => response.arrayBuffer())
.then((response) => {
this.editor.instance.on_font_load(triggerFontLoad.font, new Uint8Array(response), false);
});
this.editor.dispatcher.subscribeJsMessage(TriggerFontLoad, async (triggerFontLoad) => {
const response = await fetch(triggerFontLoad.font);
const responseBuffer = await response.arrayBuffer();
this.editor.instance.on_font_load(triggerFontLoad.font, new Uint8Array(responseBuffer), false);
});
this.editor.dispatcher.subscribeJsMessage(TriggerDefaultFontLoad, loadDefaultFont);
this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, async (triggerTextCopy) => {
// Clipboard API supported?
if (!navigator.clipboard) return;
// copy text to clipboard
if (navigator.clipboard.writeText) {
await navigator.clipboard.writeText(triggerTextCopy.copy_text);
}
this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, (triggerTextCopy) => {
// If the Clipboard API is supported in the browser, copy text to the clipboard
navigator.clipboard?.writeText?.(triggerTextCopy.copy_text);
});
this.editor.dispatcher.subscribeJsMessage(DisplayEditableTextbox, (displayEditableTextbox) => {
@ -511,15 +508,15 @@ export default defineComponent({
this.editor.dispatcher.subscribeJsMessage(TriggerViewportResize, this.viewportResize);
this.editor.dispatcher.subscribeJsMessage(UpdateImageData, (updateImageData) => {
updateImageData.image_data.forEach((element) => {
updateImageData.image_data.forEach(async (element) => {
// Using updateImageData.image_data.buffer returns undefined for some reason?
const blob = new Blob([new Uint8Array(element.image_data.values()).buffer], { type: element.mime });
const url = URL.createObjectURL(blob);
createImageBitmap(blob).then((image) => {
this.editor.instance.set_image_blob_url(element.path, url, image.width, image.height);
});
const image = await createImageBitmap(blob);
this.editor.instance.set_image_blob_url(element.path, url, image.width, image.height);
});
});

View file

@ -42,11 +42,11 @@
class="layer-row"
v-for="(listing, index) in layers"
:key="String(listing.entry.path.slice(-1))"
:class="{ 'insert-folder': draggingData && draggingData.highlightFolder && draggingData.insertFolder === listing.entry.path }"
:class="{ 'insert-folder': draggingData?.highlightFolder && draggingData?.insertFolder === listing.entry.path }"
>
<LayoutRow class="visibility">
<IconButton
:action="(e) => (toggleLayerVisibility(listing.entry.path), e && e.stopPropagation())"
:action="(e) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())"
:size="24"
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
:title="listing.entry.visible ? 'Visible' : 'Hidden'"
@ -396,16 +396,16 @@ export default defineComponent({
markTopOffset(height: number): string {
return `${height}px`;
},
async createEmptyFolder() {
createEmptyFolder() {
this.editor.instance.create_empty_folder();
},
async deleteSelectedLayers() {
deleteSelectedLayers() {
this.editor.instance.delete_selected_layers();
},
async toggleLayerVisibility(path: BigUint64Array) {
toggleLayerVisibility(path: BigUint64Array) {
this.editor.instance.toggle_layer_visibility(path);
},
async handleExpandArrowClick(path: BigUint64Array) {
handleExpandArrowClick(path: BigUint64Array) {
this.editor.instance.toggle_layer_expansion(path);
},
onEditLayerName(listing: LayerListingInfo) {
@ -414,12 +414,12 @@ export default defineComponent({
this.draggable = false;
listing.editingName = true;
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement;
const tree: HTMLElement = (this.$refs.layerTreeList as typeof LayoutCol).$el;
this.$nextTick(() => {
(tree.querySelector("[data-text-input]:not([disabled])") as HTMLInputElement).select();
});
},
async onEditLayerNameChange(listing: LayerListingInfo, inputElement: EventTarget | null) {
onEditLayerNameChange(listing: LayerListingInfo, inputElement: EventTarget | null) {
// Eliminate duplicate events
if (!listing.editingName) return;
@ -434,8 +434,7 @@ export default defineComponent({
listing.editingName = false;
this.$nextTick(() => {
const selection = window.getSelection();
if (selection) selection.removeAllRanges();
window.getSelection()?.removeAllRanges();
});
},
async setLayerBlendMode(newSelectedIndex: number) {
@ -536,7 +535,7 @@ export default defineComponent({
// Stop the drag from being shown as cancelled
event.preventDefault();
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement;
const tree: HTMLElement = (this.$refs.layerTreeList as typeof LayoutCol).$el;
this.draggingData = this.calculateDragIndex(tree, event.clientY);
},
async drop() {
@ -594,8 +593,8 @@ export default defineComponent({
mounted() {
this.editor.dispatcher.subscribeJsMessage(DisplayDocumentLayerTreeStructure, (displayDocumentLayerTreeStructure) => {
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
const layerPathWithNameBeingEdited = layerWithNameBeingEdited && layerWithNameBeingEdited.entry.path;
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited && layerPathWithNameBeingEdited.slice(-1)[0];
const layerPathWithNameBeingEdited = layerWithNameBeingEdited?.entry.path;
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited?.slice(-1)[0];
const path = [] as bigint[];
this.layers = [] as LayerListingInfo[];
@ -606,9 +605,18 @@ export default defineComponent({
path.push(layerId);
const mapping = cache.get(path.toString());
if (mapping) layers.push({ folderIndex: index, bottomLayer: index === folder.children.length - 1, entry: mapping, editingName: layerIdWithNameBeingEdited === layerId });
if (mapping) {
layers.push({
folderIndex: index,
bottomLayer: index === folder.children.length - 1,
entry: mapping,
editingName: layerIdWithNameBeingEdited === layerId,
});
}
// Call self recursively if there are any children
if (item.children.length >= 1) recurse(item, layers, cache);
path.pop();
});
};
@ -626,6 +634,7 @@ export default defineComponent({
} else {
this.layerCache.set(targetPath.toString(), targetLayer);
}
this.setBlendModeForSelectedLayers();
this.setOpacityForSelectedLayers();
});

View file

@ -1,5 +1,4 @@
<template>
<div>{{ widgetData.name }}</div>
<div class="widget-row">
<template v-for="(component, index) in widgetData.widgets" :key="index">
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->

View file

@ -69,7 +69,7 @@ export default defineComponent({
handleClick() {
(this.$refs.floatingMenu as typeof FloatingMenu).setOpen();
if (this.action) this.action();
this.action?.();
},
},
});

View file

@ -161,13 +161,13 @@ export default defineComponent({
},
onPointerDown(e: PointerEvent) {
const saturationPicker = this.$refs.saturationPicker as typeof LayoutCol;
const saturationPickerElement = saturationPicker && (saturationPicker.$el as HTMLElement);
const saturationPickerElement = saturationPicker?.$el as HTMLElement | undefined;
const huePicker = this.$refs.huePicker as typeof LayoutCol;
const huePickerElement = huePicker && (huePicker.$el as HTMLElement);
const huePickerElement = huePicker?.$el as HTMLElement | undefined;
const opacityPicker = this.$refs.opacityPicker as typeof LayoutCol;
const opacityPickerElement = opacityPicker && (opacityPicker.$el as HTMLElement);
const opacityPickerElement = opacityPicker?.$el as HTMLElement | undefined;
if (!(e.currentTarget instanceof HTMLElement) || !saturationPickerElement || !huePickerElement || !opacityPickerElement) return;
@ -216,13 +216,13 @@ export default defineComponent({
},
updateRects() {
const saturationPicker = this.$refs.saturationPicker as typeof LayoutCol;
const saturationPickerElement = saturationPicker && (saturationPicker.$el as HTMLElement);
const saturationPickerElement = saturationPicker?.$el as HTMLElement | undefined;
const huePicker = this.$refs.huePicker as typeof LayoutCol;
const huePickerElement = huePicker && (huePicker.$el as HTMLElement);
const huePickerElement = huePicker?.$el as HTMLElement | undefined;
const opacityPicker = this.$refs.opacityPicker as typeof LayoutCol;
const opacityPickerElement = opacityPicker && (opacityPicker.$el as HTMLElement);
const opacityPickerElement = opacityPicker?.$el as HTMLElement | undefined;
if (!saturationPickerElement || !huePickerElement || !opacityPickerElement) return;

View file

@ -9,7 +9,7 @@
<TextLabel :bold="true" class="heading">{{ dialog.state.heading }}</TextLabel>
<TextLabel class="details">{{ dialog.state.details }}</TextLabel>
<LayoutRow class="buttons-row" v-if="dialog.state.buttons.length > 0">
<TextButton v-for="(button, index) in dialog.state.buttons" :key="index" :title="button.tooltip" :action="() => button.callback && button.callback()" v-bind="button.props" />
<TextButton v-for="(button, index) in dialog.state.buttons" :key="index" :title="button.tooltip" :action="() => button.callback?.()" v-bind="button.props" />
</LayoutRow>
</LayoutCol>
</LayoutRow>

View file

@ -212,7 +212,7 @@ export default defineComponent({
const workspace = document.querySelector("[data-workspace]");
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol;
const floatingMenuContent = floatingMenuContentComponent && (floatingMenuContentComponent.$el as HTMLElement);
const floatingMenuContent: HTMLElement | undefined = floatingMenuContentComponent?.$el;
const floatingMenu = this.$refs.floatingMenu as HTMLElement;
if (!workspace || !floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !floatingMenu) return;
@ -298,30 +298,28 @@ export default defineComponent({
},
getWidth(callback: (width: number) => void) {
this.$nextTick(() => {
const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement;
const floatingMenuContent: HTMLElement = (this.$refs.floatingMenuContent as typeof LayoutCol).$el;
const width = floatingMenuContent.clientWidth;
callback(width);
});
},
disableMinWidth(callback: (minWidth: string) => void) {
this.$nextTick(() => {
const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement;
const floatingMenuContent: HTMLElement = (this.$refs.floatingMenuContent as typeof LayoutCol).$el;
const initialMinWidth = floatingMenuContent.style.minWidth;
floatingMenuContent.style.minWidth = "0";
callback(initialMinWidth);
});
},
enableMinWidth(minWidth: string) {
const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement;
const floatingMenuContent: HTMLElement = (this.$refs.floatingMenuContent as typeof LayoutCol).$el;
floatingMenuContent.style.minWidth = minWidth;
},
pointerMoveHandler(e: PointerEvent) {
const target = e.target as HTMLElement;
const pointerOverFloatingMenuKeepOpen = target && (target.closest("[data-hover-menu-keep-open]") as HTMLElement);
const pointerOverFloatingMenuSpawner = target && (target.closest("[data-hover-menu-spawner]") as HTMLElement);
// TODO: Simplify the following expression when optional chaining is supported by the build system
const pointerOverOwnFloatingMenuSpawner =
pointerOverFloatingMenuSpawner && pointerOverFloatingMenuSpawner.parentElement && pointerOverFloatingMenuSpawner.parentElement.contains(this.$refs.floatingMenu as HTMLElement);
const target = e.target as HTMLElement | undefined;
const pointerOverFloatingMenuKeepOpen = target?.closest("[data-hover-menu-keep-open]") as HTMLElement | undefined;
const pointerOverFloatingMenuSpawner = target?.closest("[data-hover-menu-spawner]") as HTMLElement | undefined;
const pointerOverOwnFloatingMenuSpawner = pointerOverFloatingMenuSpawner?.parentElement?.contains(this.$refs.floatingMenu as HTMLElement);
// Swap this open floating menu with the one created by the floating menu spawner being hovered over
if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) {
this.setClosed();
@ -372,10 +370,12 @@ export default defineComponent({
},
isPointerEventOutsideMenuElement(e: PointerEvent, element: HTMLElement, extraDistanceAllowed = 0): boolean {
const floatingMenuBounds = element.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;
},
},

View file

@ -19,9 +19,9 @@
<span class="entry-label">{{ entry.label }}</span>
<IconLabel v-if="entry.shortcutRequiresLock && !fullscreen.state.keyboardLocked" :icon="'Info'" :title="keyboardLockInfoMessage" />
<UserInputLabel v-else-if="entry.shortcut && entry.shortcut.length" :inputKeys="[entry.shortcut]" />
<UserInputLabel v-else-if="entry.shortcut?.length" :inputKeys="[entry.shortcut]" />
<div class="submenu-arrow" v-if="entry.children && entry.children.length"></div>
<div class="submenu-arrow" v-if="entry.children?.length"></div>
<div class="no-submenu-arrow" v-else></div>
<MenuList
@ -175,10 +175,10 @@ const MenuList = defineComponent({
scrollableY: { type: Boolean as PropType<boolean>, default: false },
},
methods: {
setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu) {
setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu): void {
if (ref) menuEntry.ref = ref;
},
handleEntryClick(menuEntry: MenuListEntry) {
handleEntryClick(menuEntry: MenuListEntry): void {
(this.$refs.floatingMenu as typeof FloatingMenu).setClosed();
if (menuEntry.checkbox) menuEntry.checked = !menuEntry.checked;
@ -188,20 +188,20 @@ const MenuList = defineComponent({
this.$emit("update:activeEntry", menuEntry);
},
handleEntryPointerEnter(menuEntry: MenuListEntry) {
if (!menuEntry.children || !menuEntry.children.length) return;
handleEntryPointerEnter(menuEntry: MenuListEntry): void {
if (!menuEntry.children?.length) return;
if (menuEntry.ref) menuEntry.ref.setOpen();
else throw new Error("The menu bar floating menu has no associated ref");
},
handleEntryPointerLeave(menuEntry: MenuListEntry) {
if (!menuEntry.children || !menuEntry.children.length) return;
handleEntryPointerLeave(menuEntry: MenuListEntry): void {
if (!menuEntry.children?.length) return;
if (menuEntry.ref) menuEntry.ref.setClosed();
else throw new Error("The menu bar floating menu has no associated ref");
},
isMenuEntryOpen(menuEntry: MenuListEntry): boolean {
if (!menuEntry.children || !menuEntry.children.length) return false;
if (!menuEntry.children?.length) return false;
if (menuEntry.ref) return menuEntry.ref.isOpen();
@ -215,7 +215,7 @@ const MenuList = defineComponent({
},
isOpen(): boolean {
const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu;
return Boolean(floatingMenu && floatingMenu.isOpen());
return Boolean(floatingMenu?.isOpen());
},
async measureAndReportWidth() {
// API is experimental but supported in all browsers - https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready
@ -242,10 +242,8 @@ const MenuList = defineComponent({
},
computed: {
menuEntriesWithoutRefs(): MenuListEntryData[][] {
const { menuEntries } = this;
return menuEntries.map((entries) =>
return this.menuEntries.map((entries) =>
entries.map((entry) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ref, ...entryWithoutRef } = entry;
return entryWithoutRef;
})

View file

@ -112,34 +112,45 @@ export default defineComponent({
if (!this.disabled) (this.$refs.menuList as typeof MenuList).setOpen();
},
selectFont(newName: string) {
if (this.isStyle) this.$emit("update:fontStyle", newName);
else this.$emit("update:fontFamily", newName);
let fontFamily;
let fontStyle;
{
const fontFamily = this.isStyle ? this.fontFamily : newName;
const fontStyle = this.isStyle ? newName : getFontStyles(newName)[0];
const fontFile = getFontFile(fontFamily, fontStyle);
this.$emit("changeFont", { fontFamily, fontStyle, fontFile });
if (this.isStyle) {
this.$emit("update:fontStyle", newName);
fontFamily = this.fontFamily;
fontStyle = newName;
} else {
this.$emit("update:fontFamily", newName);
fontFamily = newName;
fontStyle = getFontStyles(newName)[0];
}
const fontFile = getFontFile(fontFamily, fontStyle);
this.$emit("changeFont", { fontFamily, fontStyle, fontFile });
},
onWidthChanged(newWidth: number) {
this.minWidth = newWidth;
},
updateEntries(): { menuEntries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } {
let selectedIndex = -1;
const menuEntries: SectionsOfMenuListEntries = [
(this.isStyle ? getFontStyles(this.fontFamily) : fontNames()).map((name, index) => {
if (name === (this.isStyle ? this.fontStyle : this.fontFamily)) selectedIndex = index;
const choices = this.isStyle ? getFontStyles(this.fontFamily) : fontNames();
const selectedChoice = this.isStyle ? this.fontStyle : this.fontFamily;
const result: MenuListEntry = {
label: name,
action: (): void => this.selectFont(name),
};
return result;
}),
];
let selectedEntry: MenuListEntry | undefined;
const entries = choices.map((name) => {
const result: MenuListEntry = {
label: name,
action: (): void => this.selectFont(name),
};
const activeEntry = selectedIndex < 0 ? { label: "-" } : menuEntries.flat()[selectedIndex];
if (name === selectedChoice) selectedEntry = result;
return result;
});
const menuEntries: SectionsOfMenuListEntries = [entries];
const activeEntry = selectedEntry || { label: "-" };
return { menuEntries, activeEntry };
},

View file

@ -6,7 +6,7 @@
</div>
</div>
<div class="entry-container" v-for="(entry, index) in menuEntries" :key="index">
<div @click="() => handleEntryClick(entry)" class="entry" :class="{ open: entry.ref && entry.ref.isOpen() }" data-hover-menu-spawner>
<div @click="() => handleEntryClick(entry)" class="entry" :class="{ open: entry.ref?.isOpen() }" data-hover-menu-spawner>
<IconLabel :icon="entry.icon" v-if="entry.icon" />
<span v-if="entry.label">{{ entry.label }}</span>
</div>

View file

@ -152,7 +152,7 @@ export default defineComponent({
onIncrement(direction: IncrementDirection) {
if (Number.isNaN(this.value)) return;
({
const actions = {
Add: (): void => {
const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor;
this.updateValue(this.value + directionAddend);
@ -162,11 +162,13 @@ export default defineComponent({
this.updateValue(this.value * directionMultiplier);
},
Callback: (): void => {
if (direction === "Increase" && this.incrementCallbackIncrease) this.incrementCallbackIncrease();
if (direction === "Decrease" && this.incrementCallbackDecrease) this.incrementCallbackDecrease();
if (direction === "Increase") this.incrementCallbackIncrease?.();
if (direction === "Decrease") this.incrementCallbackDecrease?.();
},
None: (): void => undefined,
}[this.incrementBehavior]());
};
const action = actions[this.incrementBehavior];
action();
},
updateValue(newValue: number) {
const invalid = Number.isNaN(newValue);

View file

@ -91,7 +91,7 @@ export default defineComponent({
const index = this.entries.indexOf(menuEntry);
this.$emit("update:selectedIndex", index);
if (menuEntry.action) menuEntry.action();
menuEntry.action?.();
},
},
components: {

View file

@ -27,14 +27,12 @@
</style>
<script lang="ts">
import { DefineComponent, defineComponent, PropType } from "vue";
import { defineComponent, PropType } from "vue";
import { IconName, IconSize, ICON_LIST } from "@/utilities/icons";
import { IconName, icons } from "@/utilities/icons";
import LayoutRow from "@/components/layout/LayoutRow.vue";
const icons: Record<IconName, { component: DefineComponent; size: IconSize }> = ICON_LIST;
export default defineComponent({
props: {
icon: { type: String as PropType<IconName>, required: true },
@ -47,6 +45,7 @@ export default defineComponent({
},
components: {
LayoutRow,
// Import the components of all the icons
...Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component])),
},
});

View file

@ -52,7 +52,7 @@ const MINOR_MARK_THICKNESS = 3;
export type RulerDirection = "Horizontal" | "Vertical";
// Apparently the modulo operator in js does not work properly.
// Modulo function that works for negative numbers, unlike the JS % operator
const mod = (n: number, m: number): number => {
const remainder = n % m;
return Math.floor(remainder >= 0 ? remainder : remainder + m);

View file

@ -190,9 +190,7 @@ export default defineComponent({
this.dragging = false;
},
pointerMove(e: PointerEvent) {
if (this.dragging) {
this.updateHandlePosition(e);
}
if (this.dragging) this.updateHandlePosition(e);
},
changePosition(difference: number) {
this.clampHandlePosition(this.handlePosition + difference / this.trackLength());

View file

@ -8,11 +8,11 @@
data-tab
v-for="(tabLabel, tabIndex) in tabLabels"
:key="tabIndex"
@click="(e) => (e && e.stopPropagation(), clickAction && clickAction(tabIndex))"
@click.middle="(e) => (e && e.stopPropagation(), closeAction && closeAction(tabIndex))"
@click="(e) => (e?.stopPropagation(), clickAction?.(tabIndex))"
@click.middle="(e) => (e?.stopPropagation(), closeAction?.(tabIndex))"
>
<span>{{ tabLabel }}</span>
<IconButton :action="(e) => (e && e.stopPropagation(), closeAction && closeAction(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
<IconButton :action="(e) => (e?.stopPropagation(), closeAction?.(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
</LayoutRow>
</LayoutRow>
<PopoverButton :icon="'VerticalEllipsis'">

View file

@ -7,18 +7,8 @@
:tabCloseButtons="true"
:tabMinWidths="true"
:tabLabels="documents.state.documents.map((doc) => doc.displayName)"
:clickAction="
(tabIndex) => {
const targetId = documents.state.documents[tabIndex].id;
editor.instance.select_document(targetId);
}
"
:closeAction="
(tabIndex) => {
const targetId = documents.state.documents[tabIndex].id;
editor.instance.close_document_with_confirmation(targetId);
}
"
:clickAction="(tabIndex) => editor.instance.select_document(documents.state.documents[tabIndex].id)"
:closeAction="(tabIndex) => editor.instance.close_document_with_confirmation(documents.state.documents[tabIndex].id)"
:tabActiveIndex="documents.state.activeDocumentIndex"
ref="documentsPanel"
/>

View file

@ -1,6 +1,6 @@
import { plainToInstance } from "class-transformer";
import { JsMessageType, messageConstructors, JsMessage } from "@/dispatcher/js-messages";
import { JsMessageType, messageMakers, JsMessage } from "@/dispatcher/js-messages";
import type { RustEditorInstance, WasmInstance } from "@/state/wasm-loader";
type JsMessageCallback<T extends JsMessage> = (messageData: T) => void;
@ -20,36 +20,41 @@ export function createJsDispatcher() {
};
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WasmInstance, instance: RustEditorInstance): void => {
const messageConstructor = messageConstructors[messageType];
if (!messageConstructor) {
// Find the message maker for the message type, which can either be a JS class constructor or a function that returns an instance of the JS class
const messageMaker = messageMakers[messageType];
if (!messageMaker) {
// eslint-disable-next-line no-console
console.error(
`Received a frontend message of type "${messageType}" but was not able to parse the data. ` +
"(Perhaps this message parser isn't exported in `messageConstructors` at the bottom of `js-messages.ts`.)"
"(Perhaps this message parser isn't exported in `messageMakers` at the bottom of `js-messages.ts`.)"
);
return;
}
// Checks if the provided `messageMaker` is a class extending `JsMessage`. All classes inheriting from `JsMessage` will have a static readonly `jsMessageMarker` which is `true`.
const isJsMessageMaker = (fn: typeof messageMaker): fn is typeof JsMessage => "jsMessageMarker" in fn;
const messageIsClass = isJsMessageMaker(messageMaker);
// Messages with non-empty data are provided by wasm-bindgen as an object with one key as the message name, like: { NameOfThisMessage: { ... } }
// Messages with empty data are provided by wasm-bindgen as a string with the message name, like: "NameOfThisMessage"
// Here we extract the payload object or use an empty object depending on the situation.
const unwrappedMessageData = messageData[messageType] || {};
const isJsMessageConstructor = (fn: typeof messageConstructor): fn is typeof JsMessage => "jsMessageMarker" in fn;
let message: JsMessage;
if (isJsMessageConstructor(messageConstructor)) {
message = plainToInstance(messageConstructor, unwrappedMessageData);
} else {
message = messageConstructor(unwrappedMessageData, wasm, instance);
}
// Converts to a `JsMessage` object by turning the JSON message data into an instance of the message class, either automatically or by calling the function that builds it.
// If the `messageMaker` is a `JsMessage` class then we use the class-transformer library's `plainToInstance` function in order to convert the JSON data into the destination class.
// If it is not a `JsMessage` then it should be a custom function that creates a JsMessage from a JSON, so we call the function itself with the raw JSON as an argument.
// The resulting `message` is an instance of a class that extends `JsMessage`.
const message = messageIsClass ? plainToInstance(messageMaker, unwrappedMessageData) : messageMaker(unwrappedMessageData, wasm, instance);
// It is ok to use constructor.name even with minification since it is used consistently with registerHandler
const callback = subscriptions[message.constructor.name];
if (callback && message) {
callback(message);
} else if (message) {
// If we have constructed a valid message, then we try and execute the callback that the frontend has associated with this message.
// The frontend should always have a callback for all messages, and so we display an error if one was not found.
if (message) {
if (callback) callback(message);
// eslint-disable-next-line no-console
console.error(`Received a frontend message of type "${messageType}" but no handler was registered for it from the client.`);
else console.error(`Received a frontend message of type "${messageType}" but no handler was registered for it from the client.`);
}
};

View file

@ -244,9 +244,9 @@ interface DataBuffer {
}
export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayDocumentLayerTreeStructure {
const { pointer, length } = input.data_buffer;
const pointerNum = Number(pointer);
const lengthNum = Number(length);
const pointerNum = Number(input.data_buffer.pointer);
const lengthNum = Number(input.data_buffer.length);
const wasmMemoryBuffer = wasm.wasm_memory().buffer;
// Decode the folder structure encoding
@ -265,7 +265,7 @@ export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataB
for (let i = 0; i < structureSectionLength; i += 1) {
const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true);
const msbMask = BigInt(1) << BigInt(63);
const msbMask = BigInt(1) << BigInt(64 - 1);
// Set the MSB to 0 to clear the sign and then read the number as usual
const numberOfLayersAtThisDepth = msbSigned & ~msbMask;
@ -485,7 +485,7 @@ export class UpdatePropertyPanelSectionsLayout extends JsMessage implements Widg
// Unpacking rust types to more usable type in the frontend
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
return widgetLayout.map((rowOrSection) => {
return widgetLayout.map((rowOrSection): LayoutRow => {
if (rowOrSection.Row) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const widgets = rowOrSection.Row.widgets.map((widgetHolder: any) => {
@ -496,16 +496,16 @@ function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
return { widget_id, kind, props };
});
return {
name: rowOrSection.Row.name,
widgets,
};
const result: WidgetRow = { widgets };
return result;
}
if (rowOrSection.Section) {
return {
name: rowOrSection.Section.name,
layout: createWidgetLayout(rowOrSection.Section.layout),
};
const { name } = rowOrSection.Section;
const layout = createWidgetLayout(rowOrSection.Section.layout);
const result: WidgetSection = { name, layout };
return result;
}
throw new Error("Layout row type does not exist");
@ -524,12 +524,12 @@ export class TriggerTextCopy extends JsMessage {
export class TriggerViewportResize extends JsMessage {}
// Any is used since the type of the object should be known from the rust side
// `any` is used since the type of the object should be known from the Rust side
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInstance) => JsMessage;
type MessageMaker = typeof JsMessage | JSMessageFactory;
export const messageConstructors: Record<string, MessageMaker> = {
export const messageMakers: Record<string, MessageMaker> = {
DisplayConfirmationToCloseAllDocuments,
DisplayConfirmationToCloseDocument,
DisplayDialogAboutGraphite,
@ -568,4 +568,4 @@ export const messageConstructors: Record<string, MessageMaker> = {
UpdateToolOptionsLayout,
UpdateWorkingColors,
} as const;
export type JsMessageType = keyof typeof messageConstructors;
export type JsMessageType = keyof typeof messageMakers;

View file

@ -86,7 +86,5 @@ export function createAutoSaveManager(editor: EditorState, documents: DocumentsS
// On creation
openAutoSavedDocuments();
return {
openAutoSavedDocuments,
};
return { openAutoSavedDocuments };
}

View file

@ -42,9 +42,14 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
if (!key) return false;
// Don't redirect user input from text entry into HTML elements
const { target } = e;
if (key !== "escape" && !(key === "enter" && e.ctrlKey) && target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable))
if (
key !== "escape" &&
!(key === "enter" && e.ctrlKey) &&
e.target instanceof HTMLElement &&
(e.target.nodeName === "INPUT" || e.target.nodeName === "TEXTAREA" || e.target.isContentEditable)
) {
return false;
}
// Don't redirect paste
if (key === "v" && e.ctrlKey) return false;
@ -124,9 +129,10 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
e.stopPropagation();
}
if (textInput && !inTextInput) {
editor.instance.on_change_text(textInputCleanup(textInput.innerText));
} else if (inCanvas && !inTextInput) viewportPointerInteractionOngoing = true;
if (!inTextInput) {
if (textInput) editor.instance.on_change_text(textInputCleanup(textInput.innerText));
else if (inCanvas) viewportPointerInteractionOngoing = true;
}
if (viewportPointerInteractionOngoing) {
const modifiers = makeModifiersBitfield(e);
@ -164,6 +170,8 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
const { target } = e;
const inCanvas = target instanceof Element && target.closest("[data-canvas]");
// Redirect vertical scroll wheel movement into a horizontal scroll on a horizontally scrollable element
// There seems to be no possible way to properly employ the browser's smooth scrolling interpolation
const horizontalScrollableElement = target instanceof Element && target.closest("[data-scrollable-x]");
if (horizontalScrollableElement && e.deltaY !== 0) {
horizontalScrollableElement.scrollTo(horizontalScrollableElement.scrollLeft + e.deltaY, 0);
@ -228,7 +236,7 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
}
const file = item.getAsFile();
if (file && file.type.startsWith("image")) {
if (file?.type.startsWith("image")) {
file.arrayBuffer().then((buffer): void => {
const u8Array = new Uint8Array(buffer);
@ -252,9 +260,7 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
addListeners();
onWindowResize(container);
return {
removeListeners,
};
return { removeListeners };
}
export type InputManager = ReturnType<typeof createInputManager>;
@ -296,335 +302,336 @@ function keyCodeToKey(code: string): string | null {
if (code.match(/^F[1-9]|F1[0-9]|F20$/)) return code.replace("F", "").toLowerCase();
// Other characters
const mapping: Record<string, string> = {
BracketLeft: "[",
BracketRight: "]",
Backslash: "\\",
Slash: "/",
Period: ".",
Comma: ",",
Equal: "=",
Minus: "-",
Quote: "'",
Semicolon: ";",
NumpadEqual: "=",
NumpadDivide: "/",
NumpadMultiply: "*",
NumpadSubtract: "-",
NumpadAdd: "+",
NumpadDecimal: ".",
};
if (code in mapping) return mapping[code];
if (MAPPING[code]) return MAPPING[code];
return null;
}
function isKeyPrintable(key: string): boolean {
const allPrintableKeys: string[] = [
// Modifier
"Alt",
"AltGraph",
"CapsLock",
"Control",
"Fn",
"FnLock",
"Meta",
"NumLock",
"ScrollLock",
"Shift",
"Symbol",
"SymbolLock",
// Legacy modifier
"Hyper",
"Super",
// White space
"Enter",
"Tab",
// Navigation
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"End",
"Home",
"PageDown",
"PageUp",
// Editing
"Backspace",
"Clear",
"Copy",
"CrSel",
"Cut",
"Delete",
"EraseEof",
"ExSel",
"Insert",
"Paste",
"Redo",
"Undo",
// UI
"Accept",
"Again",
"Attn",
"Cancel",
"ContextMenu",
"Escape",
"Execute",
"Find",
"Help",
"Pause",
"Play",
"Props",
"Select",
"ZoomIn",
"ZoomOut",
// Device
"BrightnessDown",
"BrightnessUp",
"Eject",
"LogOff",
"Power",
"PowerOff",
"PrintScreen",
"Hibernate",
"Standby",
"WakeUp",
// IME composition keys
"AllCandidates",
"Alphanumeric",
"CodeInput",
"Compose",
"Convert",
"Dead",
"FinalMode",
"GroupFirst",
"GroupLast",
"GroupNext",
"GroupPrevious",
"ModeChange",
"NextCandidate",
"NonConvert",
"PreviousCandidate",
"Process",
"SingleCandidate",
// Korean-specific
"HangulMode",
"HanjaMode",
"JunjaMode",
// Japanese-specific
"Eisu",
"Hankaku",
"Hiragana",
"HiraganaKatakana",
"KanaMode",
"KanjiMode",
"Katakana",
"Romaji",
"Zenkaku",
"ZenkakuHankaku",
// Common function
"F1",
"F2",
"F3",
"F4",
"F5",
"F6",
"F7",
"F8",
"F9",
"F10",
"F11",
"F12",
"Soft1",
"Soft2",
"Soft3",
"Soft4",
// Multimedia
"ChannelDown",
"ChannelUp",
"Close",
"MailForward",
"MailReply",
"MailSend",
"MediaClose",
"MediaFastForward",
"MediaPause",
"MediaPlay",
"MediaPlayPause",
"MediaRecord",
"MediaRewind",
"MediaStop",
"MediaTrackNext",
"MediaTrackPrevious",
"New",
"Open",
"Print",
"Save",
"SpellCheck",
// Multimedia numpad
"Key11",
"Key12",
// Audio
"AudioBalanceLeft",
"AudioBalanceRight",
"AudioBassBoostDown",
"AudioBassBoostToggle",
"AudioBassBoostUp",
"AudioFaderFront",
"AudioFaderRear",
"AudioSurroundModeNext",
"AudioTrebleDown",
"AudioTrebleUp",
"AudioVolumeDown",
"AudioVolumeUp",
"AudioVolumeMute",
"MicrophoneToggle",
"MicrophoneVolumeDown",
"MicrophoneVolumeUp",
"MicrophoneVolumeMute",
// Speech
"SpeechCorrectionList",
"SpeechInputToggle",
// Application
"LaunchApplication1",
"LaunchApplication2",
"LaunchCalendar",
"LaunchContacts",
"LaunchMail",
"LaunchMediaPlayer",
"LaunchMusicPlayer",
"LaunchPhone",
"LaunchScreenSaver",
"LaunchSpreadsheet",
"LaunchWebBrowser",
"LaunchWebCam",
"LaunchWordProcessor",
// Browser
"BrowserBack",
"BrowserFavorites",
"BrowserForward",
"BrowserHome",
"BrowserRefresh",
"BrowserSearch",
"BrowserStop",
// Mobile phone
"AppSwitch",
"Call",
"Camera",
"CameraFocus",
"EndCall",
"GoBack",
"GoHome",
"HeadsetHook",
"LastNumberRedial",
"Notification",
"MannerMode",
"VoiceDial",
// TV
"TV",
"TV3DMode",
"TVAntennaCable",
"TVAudioDescription",
"TVAudioDescriptionMixDown",
"TVAudioDescriptionMixUp",
"TVContentsMenu",
"TVDataService",
"TVInput",
"TVInputComponent1",
"TVInputComponent2",
"TVInputComposite1",
"TVInputComposite2",
"TVInputHDMI1",
"TVInputHDMI2",
"TVInputHDMI3",
"TVInputHDMI4",
"TVInputVGA1",
"TVMediaContext",
"TVNetwork",
"TVNumberEntry",
"TVPower",
"TVRadioService",
"TVSatellite",
"TVSatelliteBS",
"TVSatelliteCS",
"TVSatelliteToggle",
"TVTerrestrialAnalog",
"TVTerrestrialDigital",
"TVTimer",
// Media controls
"AVRInput",
"AVRPower",
"ColorF0Red",
"ColorF1Green",
"ColorF2Yellow",
"ColorF3Blue",
"ColorF4Grey",
"ColorF5Brown",
"ClosedCaptionToggle",
"Dimmer",
"DisplaySwap",
"DVR",
"Exit",
"FavoriteClear0",
"FavoriteClear1",
"FavoriteClear2",
"FavoriteClear3",
"FavoriteRecall0",
"FavoriteRecall1",
"FavoriteRecall2",
"FavoriteRecall3",
"FavoriteStore0",
"FavoriteStore1",
"FavoriteStore2",
"FavoriteStore3",
"Guide",
"GuideNextDay",
"GuidePreviousDay",
"Info",
"InstantReplay",
"Link",
"ListProgram",
"LiveContent",
"Lock",
"MediaApps",
"MediaAudioTrack",
"MediaLast",
"MediaSkipBackward",
"MediaSkipForward",
"MediaStepBackward",
"MediaStepForward",
"MediaTopMenu",
"NavigateIn",
"NavigateNext",
"NavigateOut",
"NavigatePrevious",
"NextFavoriteChannel",
"NextUserProfile",
"OnDemand",
"Pairing",
"PinPDown",
"PinPMove",
"PinPToggle",
"PinPUp",
"PlaySpeedDown",
"PlaySpeedReset",
"PlaySpeedUp",
"RandomToggle",
"RcLowBattery",
"RecordSpeedNext",
"RfBypass",
"ScanChannelsToggle",
"ScreenModeNext",
"Settings",
"SplitScreenToggle",
"STBInput",
"STBPower",
"Subtitle",
"Teletext",
"VideoModeNext",
"Wink",
"ZoomToggle",
];
const MAPPING: Record<string, string> = {
BracketLeft: "[",
BracketRight: "]",
Backslash: "\\",
Slash: "/",
Period: ".",
Comma: ",",
Equal: "=",
Minus: "-",
Quote: "'",
Semicolon: ";",
NumpadEqual: "=",
NumpadDivide: "/",
NumpadMultiply: "*",
NumpadSubtract: "-",
NumpadAdd: "+",
NumpadDecimal: ".",
};
return !allPrintableKeys.includes(key);
function isKeyPrintable(key: string): boolean {
return !ALL_PRINTABLE_KEYS.has(key);
}
const ALL_PRINTABLE_KEYS = new Set([
// Modifier
"Alt",
"AltGraph",
"CapsLock",
"Control",
"Fn",
"FnLock",
"Meta",
"NumLock",
"ScrollLock",
"Shift",
"Symbol",
"SymbolLock",
// Legacy modifier
"Hyper",
"Super",
// White space
"Enter",
"Tab",
// Navigation
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"End",
"Home",
"PageDown",
"PageUp",
// Editing
"Backspace",
"Clear",
"Copy",
"CrSel",
"Cut",
"Delete",
"EraseEof",
"ExSel",
"Insert",
"Paste",
"Redo",
"Undo",
// UI
"Accept",
"Again",
"Attn",
"Cancel",
"ContextMenu",
"Escape",
"Execute",
"Find",
"Help",
"Pause",
"Play",
"Props",
"Select",
"ZoomIn",
"ZoomOut",
// Device
"BrightnessDown",
"BrightnessUp",
"Eject",
"LogOff",
"Power",
"PowerOff",
"PrintScreen",
"Hibernate",
"Standby",
"WakeUp",
// IME composition keys
"AllCandidates",
"Alphanumeric",
"CodeInput",
"Compose",
"Convert",
"Dead",
"FinalMode",
"GroupFirst",
"GroupLast",
"GroupNext",
"GroupPrevious",
"ModeChange",
"NextCandidate",
"NonConvert",
"PreviousCandidate",
"Process",
"SingleCandidate",
// Korean-specific
"HangulMode",
"HanjaMode",
"JunjaMode",
// Japanese-specific
"Eisu",
"Hankaku",
"Hiragana",
"HiraganaKatakana",
"KanaMode",
"KanjiMode",
"Katakana",
"Romaji",
"Zenkaku",
"ZenkakuHankaku",
// Common function
"F1",
"F2",
"F3",
"F4",
"F5",
"F6",
"F7",
"F8",
"F9",
"F10",
"F11",
"F12",
"Soft1",
"Soft2",
"Soft3",
"Soft4",
// Multimedia
"ChannelDown",
"ChannelUp",
"Close",
"MailForward",
"MailReply",
"MailSend",
"MediaClose",
"MediaFastForward",
"MediaPause",
"MediaPlay",
"MediaPlayPause",
"MediaRecord",
"MediaRewind",
"MediaStop",
"MediaTrackNext",
"MediaTrackPrevious",
"New",
"Open",
"Print",
"Save",
"SpellCheck",
// Multimedia numpad
"Key11",
"Key12",
// Audio
"AudioBalanceLeft",
"AudioBalanceRight",
"AudioBassBoostDown",
"AudioBassBoostToggle",
"AudioBassBoostUp",
"AudioFaderFront",
"AudioFaderRear",
"AudioSurroundModeNext",
"AudioTrebleDown",
"AudioTrebleUp",
"AudioVolumeDown",
"AudioVolumeUp",
"AudioVolumeMute",
"MicrophoneToggle",
"MicrophoneVolumeDown",
"MicrophoneVolumeUp",
"MicrophoneVolumeMute",
// Speech
"SpeechCorrectionList",
"SpeechInputToggle",
// Application
"LaunchApplication1",
"LaunchApplication2",
"LaunchCalendar",
"LaunchContacts",
"LaunchMail",
"LaunchMediaPlayer",
"LaunchMusicPlayer",
"LaunchPhone",
"LaunchScreenSaver",
"LaunchSpreadsheet",
"LaunchWebBrowser",
"LaunchWebCam",
"LaunchWordProcessor",
// Browser
"BrowserBack",
"BrowserFavorites",
"BrowserForward",
"BrowserHome",
"BrowserRefresh",
"BrowserSearch",
"BrowserStop",
// Mobile phone
"AppSwitch",
"Call",
"Camera",
"CameraFocus",
"EndCall",
"GoBack",
"GoHome",
"HeadsetHook",
"LastNumberRedial",
"Notification",
"MannerMode",
"VoiceDial",
// TV
"TV",
"TV3DMode",
"TVAntennaCable",
"TVAudioDescription",
"TVAudioDescriptionMixDown",
"TVAudioDescriptionMixUp",
"TVContentsMenu",
"TVDataService",
"TVInput",
"TVInputComponent1",
"TVInputComponent2",
"TVInputComposite1",
"TVInputComposite2",
"TVInputHDMI1",
"TVInputHDMI2",
"TVInputHDMI3",
"TVInputHDMI4",
"TVInputVGA1",
"TVMediaContext",
"TVNetwork",
"TVNumberEntry",
"TVPower",
"TVRadioService",
"TVSatellite",
"TVSatelliteBS",
"TVSatelliteCS",
"TVSatelliteToggle",
"TVTerrestrialAnalog",
"TVTerrestrialDigital",
"TVTimer",
// Media controls
"AVRInput",
"AVRPower",
"ColorF0Red",
"ColorF1Green",
"ColorF2Yellow",
"ColorF3Blue",
"ColorF4Grey",
"ColorF5Brown",
"ClosedCaptionToggle",
"Dimmer",
"DisplaySwap",
"DVR",
"Exit",
"FavoriteClear0",
"FavoriteClear1",
"FavoriteClear2",
"FavoriteClear3",
"FavoriteRecall0",
"FavoriteRecall1",
"FavoriteRecall2",
"FavoriteRecall3",
"FavoriteStore0",
"FavoriteStore1",
"FavoriteStore2",
"FavoriteStore3",
"Guide",
"GuideNextDay",
"GuidePreviousDay",
"Info",
"InstantReplay",
"Link",
"ListProgram",
"LiveContent",
"Lock",
"MediaApps",
"MediaAudioTrack",
"MediaLast",
"MediaSkipBackward",
"MediaSkipForward",
"MediaStepBackward",
"MediaStepForward",
"MediaTopMenu",
"NavigateIn",
"NavigateNext",
"NavigateOut",
"NavigatePrevious",
"NextFavoriteChannel",
"NextUserProfile",
"OnDemand",
"Pairing",
"PinPDown",
"PinPMove",
"PinPToggle",
"PinPUp",
"PlaySpeedDown",
"PlaySpeedReset",
"PlaySpeedUp",
"RandomToggle",
"RcLowBattery",
"RecordSpeedNext",
"RfBypass",
"ScanChannelsToggle",
"ScreenModeNext",
"Settings",
"SplitScreenToggle",
"STBInput",
"STBPower",
"Subtitle",
"Teletext",
"VideoModeNext",
"Wink",
"ZoomToggle",
]);

View file

@ -30,10 +30,7 @@ export function createDialogState(editor: EditorState) {
const submitDialog = (): void => {
const firstEmphasizedButton = state.buttons.find((button) => button.props.emphasized && button.callback);
if (firstEmphasizedButton) {
// If statement satisfies TypeScript
if (firstEmphasizedButton.callback) firstEmphasizedButton.callback();
}
firstEmphasizedButton?.callback?.();
};
const dialogIsVisible = (): boolean => state.visible;
@ -52,8 +49,7 @@ export function createDialogState(editor: EditorState) {
callback: async () => window.open(`https://github.com/GraphiteEditor/Graphite/issues/${issueNumber}`, "_blank"),
props: { label: `Issue #${issueNumber}`, minWidth: 96 },
};
const buttons = [okButton];
if (issueNumber) buttons.push(issueButton);
const buttons = issueNumber ? [okButton, issueButton] : [okButton];
createDialog("Warning", "Coming soon", details, buttons);
};
@ -65,7 +61,7 @@ export function createDialogState(editor: EditorState) {
const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
.formatToParts(new Date())
.find((part) => part.type === "timeZoneName");
const timezoneNameString = timezoneName && timezoneName.value;
const timezoneNameString = timezoneName?.value;
const hash = (process.env.VUE_APP_COMMIT_HASH || "").substring(0, 12);

View file

@ -9,18 +9,20 @@ export type RustEditorInstance = InstanceType<WasmInstance["JsEditorHandle"]>;
// `wasmImport` starts uninitialized until `initWasm()` is called in `main.ts` before the Vue app is created
let wasmImport: WasmInstance | null = null;
export async function initWasm(): Promise<void> {
// Skip if the wasm module is already initialized
if (wasmImport !== null) return;
// Separating in two lines satisfies typescript when used below
const importedWasm = await import("@/../wasm/pkg").then(panicProxy);
wasmImport = importedWasm;
// Provide a random starter seed which must occur after initializing the wasm module, since wasm can't generate is own random numbers
const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
importedWasm.set_random_seed(randomSeed);
}
// This works by proxying every function call wrapping a try-catch block to filter out redundant and confusing
// `RuntimeError: unreachable` exceptions sent to the console
// This works by proxying every function call and wrapping a try-catch block to filter out redundant and confusing
// `RuntimeError: unreachable` exceptions that would normally be printed in the browser's JS console upon a panic.
function panicProxy<T extends object>(module: T): T {
const proxyHandler = {
get(target: T, propKey: string | symbol, receiver: unknown): unknown {

View file

@ -21,12 +21,13 @@ export async function upload(acceptedEextensions: string): Promise<{ filename: s
element.addEventListener(
"change",
async () => {
if (!element.files || !element.files.length) return;
const file = element.files[0];
const filename = file.name;
const content = await file.text();
if (element.files?.length) {
const file = element.files[0];
const filename = file.name;
const content = await file.text();
resolve({ filename, content });
resolve({ filename, content });
}
},
{ capture: false, once: true }
);

View file

@ -23,12 +23,14 @@ fetch(fontListAPI)
.then((response) => response.json())
.then((json) => {
const loadedFonts = json.items as { family: string; variants: string[]; files: { [name: string]: string } }[];
fontList = loadedFonts.map((font) => {
const { family } = font;
const variants = font.variants.map(formatFontStyleName);
const files = new Map(font.variants.map((x) => [formatFontStyleName(x), font.files[x]]));
return { family, variants, files };
});
loadDefaultFont();
});
@ -48,16 +50,13 @@ function formatFontStyleName(fontStyle: string): string {
return `${weightName}${isItalic ? " Italic" : ""} (${weight})`;
}
export function loadDefaultFont(): void {
export async function loadDefaultFont(): Promise<void> {
const font = getFontFile("Merriweather", "Normal (400)");
if (!font) return;
if (font) {
fetch(font)
.then((response) => response.arrayBuffer())
.then((response) => {
if (loadDefaultFontCallback) loadDefaultFontCallback(font, new Uint8Array(response));
});
}
const response = await fetch(font);
const responseBuffer = await response.arrayBuffer();
loadDefaultFontCallback?.(font, new Uint8Array(responseBuffer));
}
export function setLoadDefaultFontCallback(callback: fontCallbackType): void {
@ -71,11 +70,11 @@ export function fontNames(): string[] {
export function getFontStyles(fontFamily: string): string[] {
const font = fontList.find((value) => value.family === fontFamily);
return font ? font.variants : [];
return font?.variants || [];
}
export function getFontFile(fontFamily: string, fontStyle: string): string | undefined {
const font = fontList.find((value) => value.family === fontFamily);
const fontFile = font && font.files.get(fontStyle);
return fontFile && fontFile.replace("http://", "https://");
const fontFile = font?.files.get(fontStyle);
return fontFile?.replace("http://", "https://");
}

View file

@ -100,109 +100,118 @@ import VectorShapeTool from "@/../assets/24px-two-tone/vector-shape-tool.svg";
import VectorSplineTool from "@/../assets/24px-two-tone/vector-spline-tool.svg";
import VectorTextTool from "@/../assets/24px-two-tone/vector-text-tool.svg";
export type IconName = keyof typeof ICON_LIST;
export type IconSize = 12 | 16 | 24;
const ICON_LIST = {
Checkmark: { component: Checkmark, size: 12 },
CloseX: { component: CloseX, size: 12 },
DropdownArrow: { component: DropdownArrow, size: 12 },
FullscreenEnter: { component: FullscreenEnter, size: 12 },
FullscreenExit: { component: FullscreenExit, size: 12 },
Grid: { component: Grid, size: 12 },
Info: { component: Info, size: 12 },
KeyboardArrowDown: { component: KeyboardArrowDown, size: 12 },
KeyboardArrowLeft: { component: KeyboardArrowLeft, size: 12 },
KeyboardArrowRight: { component: KeyboardArrowRight, size: 12 },
KeyboardArrowUp: { component: KeyboardArrowUp, size: 12 },
KeyboardBackspace: { component: KeyboardBackspace, size: 12 },
KeyboardCommand: { component: KeyboardCommand, size: 12 },
KeyboardEnter: { component: KeyboardEnter, size: 12 },
KeyboardOption: { component: KeyboardOption, size: 12 },
KeyboardShift: { component: KeyboardShift, size: 12 },
KeyboardSpace: { component: KeyboardSpace, size: 12 },
KeyboardTab: { component: KeyboardTab, size: 12 },
Link: { component: Link, size: 12 },
Overlays: { component: Overlays, size: 12 },
ResetColors: { component: ResetColors, size: 12 },
Snapping: { component: Snapping, size: 12 },
Swap: { component: Swap, size: 12 },
VerticalEllipsis: { component: VerticalEllipsis, size: 12 },
Warning: { component: Warning, size: 12 },
WindowButtonWinClose: { component: WindowButtonWinClose, size: 12 },
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: 12 },
WindowButtonWinMinimize: { component: WindowButtonWinMinimize, size: 12 },
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: 12 },
const size12: IconSize = 12;
const size16: IconSize = 16;
const size24: IconSize = 24;
AlignBottom: { component: AlignBottom, size: 16 },
AlignHorizontalCenter: { component: AlignHorizontalCenter, size: 16 },
AlignLeft: { component: AlignLeft, size: 16 },
AlignRight: { component: AlignRight, size: 16 },
AlignTop: { component: AlignTop, size: 16 },
AlignVerticalCenter: { component: AlignVerticalCenter, size: 16 },
BooleanDifference: { component: BooleanDifference, size: 16 },
BooleanIntersect: { component: BooleanIntersect, size: 16 },
BooleanSubtractBack: { component: BooleanSubtractBack, size: 16 },
BooleanSubtractFront: { component: BooleanSubtractFront, size: 16 },
BooleanUnion: { component: BooleanUnion, size: 16 },
Copy: { component: Copy, size: 16 },
EyeHidden: { component: EyeHidden, size: 16 },
EyeVisible: { component: EyeVisible, size: 16 },
File: { component: File, size: 16 },
FlipHorizontal: { component: FlipHorizontal, size: 16 },
FlipVertical: { component: FlipVertical, size: 16 },
GraphiteLogo: { component: GraphiteLogo, size: 16 },
NodeArtboard: { component: NodeArtboard, size: 16 },
NodeFolder: { component: NodeFolder, size: 16 },
NodeImage: { component: NodeImage, size: 16 },
NodeShape: { component: NodeShape, size: 16 },
NodeText: { component: NodeText, size: 16 },
Paste: { component: Paste, size: 16 },
Trash: { component: Trash, size: 16 },
ViewModeNormal: { component: ViewModeNormal, size: 16 },
ViewModeOutline: { component: ViewModeOutline, size: 16 },
ViewModePixels: { component: ViewModePixels, size: 16 },
ViewportDesignMode: { component: ViewportDesignMode, size: 16 },
ViewportGuideMode: { component: ViewportGuideMode, size: 16 },
ViewportSelectMode: { component: ViewportSelectMode, size: 16 },
ZoomIn: { component: ZoomIn, size: 16 },
ZoomOut: { component: ZoomOut, size: 16 },
ZoomReset: { component: ZoomReset, size: 16 },
export const ICON_LIST = {
Checkmark: { component: Checkmark, size: size12 },
CloseX: { component: CloseX, size: size12 },
DropdownArrow: { component: DropdownArrow, size: size12 },
FullscreenEnter: { component: FullscreenEnter, size: size12 },
FullscreenExit: { component: FullscreenExit, size: size12 },
Grid: { component: Grid, size: size12 },
Info: { component: Info, size: size12 },
KeyboardArrowDown: { component: KeyboardArrowDown, size: size12 },
KeyboardArrowLeft: { component: KeyboardArrowLeft, size: size12 },
KeyboardArrowRight: { component: KeyboardArrowRight, size: size12 },
KeyboardArrowUp: { component: KeyboardArrowUp, size: size12 },
KeyboardBackspace: { component: KeyboardBackspace, size: size12 },
KeyboardCommand: { component: KeyboardCommand, size: size12 },
KeyboardEnter: { component: KeyboardEnter, size: size12 },
KeyboardOption: { component: KeyboardOption, size: size12 },
KeyboardShift: { component: KeyboardShift, size: size12 },
KeyboardSpace: { component: KeyboardSpace, size: size12 },
KeyboardTab: { component: KeyboardTab, size: size12 },
Link: { component: Link, size: size12 },
Overlays: { component: Overlays, size: size12 },
ResetColors: { component: ResetColors, size: size12 },
Snapping: { component: Snapping, size: size12 },
Swap: { component: Swap, size: size12 },
VerticalEllipsis: { component: VerticalEllipsis, size: size12 },
Warning: { component: Warning, size: size12 },
WindowButtonWinClose: { component: WindowButtonWinClose, size: size12 },
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: size12 },
WindowButtonWinMinimize: { component: WindowButtonWinMinimize, size: size12 },
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: size12 },
MouseHintDrag: { component: MouseHintDrag, size: 16 },
MouseHintLmbDrag: { component: MouseHintLmbDrag, size: 16 },
MouseHintLmb: { component: MouseHintLmb, size: 16 },
MouseHintMmbDrag: { component: MouseHintMmbDrag, size: 16 },
MouseHintMmb: { component: MouseHintMmb, size: 16 },
MouseHintNone: { component: MouseHintNone, size: 16 },
MouseHintRmbDrag: { component: MouseHintRmbDrag, size: 16 },
MouseHintRmb: { component: MouseHintRmb, size: 16 },
MouseHintScrollDown: { component: MouseHintScrollDown, size: 16 },
MouseHintScrollUp: { component: MouseHintScrollUp, size: 16 },
AlignBottom: { component: AlignBottom, size: size16 },
AlignHorizontalCenter: { component: AlignHorizontalCenter, size: size16 },
AlignLeft: { component: AlignLeft, size: size16 },
AlignRight: { component: AlignRight, size: size16 },
AlignTop: { component: AlignTop, size: size16 },
AlignVerticalCenter: { component: AlignVerticalCenter, size: size16 },
BooleanDifference: { component: BooleanDifference, size: size16 },
BooleanIntersect: { component: BooleanIntersect, size: size16 },
BooleanSubtractBack: { component: BooleanSubtractBack, size: size16 },
BooleanSubtractFront: { component: BooleanSubtractFront, size: size16 },
BooleanUnion: { component: BooleanUnion, size: size16 },
Copy: { component: Copy, size: size16 },
EyeHidden: { component: EyeHidden, size: size16 },
EyeVisible: { component: EyeVisible, size: size16 },
File: { component: File, size: size16 },
FlipHorizontal: { component: FlipHorizontal, size: size16 },
FlipVertical: { component: FlipVertical, size: size16 },
GraphiteLogo: { component: GraphiteLogo, size: size16 },
NodeArtboard: { component: NodeArtboard, size: size16 },
NodeFolder: { component: NodeFolder, size: size16 },
NodeImage: { component: NodeImage, size: size16 },
NodeShape: { component: NodeShape, size: size16 },
NodeText: { component: NodeText, size: size16 },
Paste: { component: Paste, size: size16 },
Trash: { component: Trash, size: size16 },
ViewModeNormal: { component: ViewModeNormal, size: size16 },
ViewModeOutline: { component: ViewModeOutline, size: size16 },
ViewModePixels: { component: ViewModePixels, size: size16 },
ViewportDesignMode: { component: ViewportDesignMode, size: size16 },
ViewportGuideMode: { component: ViewportGuideMode, size: size16 },
ViewportSelectMode: { component: ViewportSelectMode, size: size16 },
ZoomIn: { component: ZoomIn, size: size16 },
ZoomOut: { component: ZoomOut, size: size16 },
ZoomReset: { component: ZoomReset, size: size16 },
GeneralArtboardTool: { component: GeneralArtboardTool, size: 24 },
GeneralEyedropperTool: { component: GeneralEyedropperTool, size: 24 },
GeneralNavigateTool: { component: GeneralNavigateTool, size: 24 },
GeneralSelectTool: { component: GeneralSelectTool, size: 24 },
GeneralFillTool: { component: GeneralFillTool, size: 24 },
GeneralGradientTool: { component: GeneralGradientTool, size: 24 },
RasterBrushTool: { component: RasterBrushTool, size: 24 },
RasterCloneTool: { component: RasterCloneTool, size: 24 },
RasterDetailTool: { component: RasterDetailTool, size: 24 },
RasterHealTool: { component: RasterHealTool, size: 24 },
RasterPatchTool: { component: RasterPatchTool, size: 24 },
RasterRelightTool: { component: RasterRelightTool, size: 24 },
VectorEllipseTool: { component: VectorEllipseTool, size: 24 },
VectorFreehandTool: { component: VectorFreehandTool, size: 24 },
VectorLineTool: { component: VectorLineTool, size: 24 },
VectorPathTool: { component: VectorPathTool, size: 24 },
VectorPenTool: { component: VectorPenTool, size: 24 },
VectorRectangleTool: { component: VectorRectangleTool, size: 24 },
VectorShapeTool: { component: VectorShapeTool, size: 24 },
VectorSplineTool: { component: VectorSplineTool, size: 24 },
VectorTextTool: { component: VectorTextTool, size: 24 },
} as const;
export const icons: IconDefinitionType<typeof ICON_LIST> = ICON_LIST;
MouseHintDrag: { component: MouseHintDrag, size: size16 },
MouseHintLmbDrag: { component: MouseHintLmbDrag, size: size16 },
MouseHintLmb: { component: MouseHintLmb, size: size16 },
MouseHintMmbDrag: { component: MouseHintMmbDrag, size: size16 },
MouseHintMmb: { component: MouseHintMmb, size: size16 },
MouseHintNone: { component: MouseHintNone, size: size16 },
MouseHintRmbDrag: { component: MouseHintRmbDrag, size: size16 },
MouseHintRmb: { component: MouseHintRmb, size: size16 },
MouseHintScrollDown: { component: MouseHintScrollDown, size: size16 },
MouseHintScrollUp: { component: MouseHintScrollUp, size: size16 },
export type IconName = keyof typeof icons;
export type IconSize = 12 | 16 | 24 | 32;
GeneralArtboardTool: { component: GeneralArtboardTool, size: size24 },
GeneralEyedropperTool: { component: GeneralEyedropperTool, size: size24 },
GeneralNavigateTool: { component: GeneralNavigateTool, size: size24 },
GeneralSelectTool: { component: GeneralSelectTool, size: size24 },
GeneralFillTool: { component: GeneralFillTool, size: size24 },
GeneralGradientTool: { component: GeneralGradientTool, size: size24 },
RasterBrushTool: { component: RasterBrushTool, size: size24 },
RasterCloneTool: { component: RasterCloneTool, size: size24 },
RasterDetailTool: { component: RasterDetailTool, size: size24 },
RasterHealTool: { component: RasterHealTool, size: size24 },
RasterPatchTool: { component: RasterPatchTool, size: size24 },
RasterRelightTool: { component: RasterRelightTool, size: size24 },
VectorEllipseTool: { component: VectorEllipseTool, size: size24 },
VectorFreehandTool: { component: VectorFreehandTool, size: size24 },
VectorLineTool: { component: VectorLineTool, size: size24 },
VectorPathTool: { component: VectorPathTool, size: size24 },
VectorPenTool: { component: VectorPenTool, size: size24 },
VectorRectangleTool: { component: VectorRectangleTool, size: size24 },
VectorShapeTool: { component: VectorShapeTool, size: size24 },
VectorSplineTool: { component: VectorSplineTool, size: size24 },
VectorTextTool: { component: VectorTextTool, size: size24 },
};
// The following helper type declarations allow us to avoid manually maintaining the `IconName` type declaration as a string union paralleling the keys of the
// icon definitions. It lets TypeScript do that for us. Our goal is to define the big icons key-value pair by constraining its values, but inferring its keys.
// Constraining its values means that TypeScript can make sure each icon definition has a valid size number from the union of numbers that is `IconSize`.
// Inferring its keys means we don't have to specify a supertype like `string` or `any` for the key-value pair's keys, which would prevent us from accessing
// the individual keys with `keyof typeof`. Absent a specified type for the keys, TypeScript falls back to inferring that the key-value pair's type is the
// map of all its individual entries. Having the full list of entries lets us automatically set the `IconName` type to the union of strings that is the full
// list of keys. The result is that we don't have to maintain a separate list of icon names since this scheme infers it from the keys of the icon definitions.
// Based on https://stackoverflow.com/a/64119715/775283
type IconDefinition = { component: string; size: IconSize };
type EvaluateType<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
type IconDefinitionType<T extends Record<string, IconDefinition>> = EvaluateType<{ [key in keyof T]: IconDefinition }>;