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
+2 -1
View File
@@ -59,6 +59,7 @@ module.exports = {
"linebreak-style": ["error", "unix"], "linebreak-style": ["error", "unix"],
"eol-last": ["error", "always"], "eol-last": ["error", "always"],
"max-len": ["error", { code: 200, tabWidth: 4 }], "max-len": ["error", { code: 200, tabWidth: 4 }],
"prefer-destructuring": "off",
"no-console": "warn", "no-console": "warn",
"no-debugger": "warn", "no-debugger": "warn",
"no-param-reassign": ["error", { props: false }], "no-param-reassign": ["error", { props: false }],
@@ -71,7 +72,7 @@ module.exports = {
"@typescript-eslint/indent": "off", "@typescript-eslint/indent": "off",
"@typescript-eslint/camelcase": "off", "@typescript-eslint/camelcase": "off",
"@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", ignoreRestSiblings: true }],
"@typescript-eslint/explicit-function-return-type": ["error"], "@typescript-eslint/explicit-function-return-type": ["error"],
// Import plugin config (used to intelligently validate module import statements) // Import plugin config (used to intelligently validate module import statements)
+2 -5
View File
@@ -318,11 +318,8 @@ export default defineComponent({
this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.documents, this.fullscreen); this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.documents, this.fullscreen);
}, },
beforeUnmount() { beforeUnmount() {
const { inputManager } = this; this.inputManager?.removeListeners();
if (inputManager) inputManager.removeListeners(); this.editor.instance.free();
const { editor } = this;
editor.instance.free();
}, },
components: { components: {
MainWindow, MainWindow,
+21 -24
View File
@@ -329,22 +329,21 @@ export default defineComponent({
const rulerHorizontal = this.$refs.rulerHorizontal as typeof CanvasRuler; const rulerHorizontal = this.$refs.rulerHorizontal as typeof CanvasRuler;
const rulerVertical = this.$refs.rulerVertical as typeof CanvasRuler; const rulerVertical = this.$refs.rulerVertical as typeof CanvasRuler;
if (rulerHorizontal) rulerHorizontal.handleResize(); rulerHorizontal?.handleResize();
if (rulerVertical) rulerVertical.handleResize(); rulerVertical?.handleResize();
}, },
pasteFile(e: DragEvent) { pasteFile(e: DragEvent) {
const { dataTransfer } = e; const { dataTransfer } = e;
if (!dataTransfer) return; if (!dataTransfer) return;
e.preventDefault(); e.preventDefault();
Array.from(dataTransfer.items).forEach((item) => { Array.from(dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile(); const file = item.getAsFile();
if (file && file.type.startsWith("image")) { if (file?.type.startsWith("image")) {
file.arrayBuffer().then((buffer): void => { const buffer = await file.arrayBuffer();
const u8Array = new Uint8Array(buffer); 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(); const range = document.createRange();
range.selectNodeContents(addedInput); range.selectNodeContents(addedInput);
const selection = window.getSelection(); const selection = window.getSelection();
if (selection) { if (selection) {
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
} }
addedInput.focus(); addedInput.focus();
addedInput.click(); addedInput.click();
}); });
@@ -455,24 +456,20 @@ export default defineComponent({
this.canvasCursor = updateMouseCursor.cursor; this.canvasCursor = updateMouseCursor.cursor;
}); });
this.editor.dispatcher.subscribeJsMessage(TriggerTextCommit, () => { 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.dispatcher.subscribeJsMessage(TriggerFontLoad, (triggerFontLoad) => { this.editor.instance.on_change_text(textCleaned);
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(TriggerDefaultFontLoad, loadDefaultFont);
this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, async (triggerTextCopy) => { this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, (triggerTextCopy) => {
// Clipboard API supported? // If the Clipboard API is supported in the browser, copy text to the clipboard
if (!navigator.clipboard) return; navigator.clipboard?.writeText?.(triggerTextCopy.copy_text);
// copy text to clipboard
if (navigator.clipboard.writeText) {
await navigator.clipboard.writeText(triggerTextCopy.copy_text);
}
}); });
this.editor.dispatcher.subscribeJsMessage(DisplayEditableTextbox, (displayEditableTextbox) => { this.editor.dispatcher.subscribeJsMessage(DisplayEditableTextbox, (displayEditableTextbox) => {
@@ -511,17 +508,17 @@ export default defineComponent({
this.editor.dispatcher.subscribeJsMessage(TriggerViewportResize, this.viewportResize); this.editor.dispatcher.subscribeJsMessage(TriggerViewportResize, this.viewportResize);
this.editor.dispatcher.subscribeJsMessage(UpdateImageData, (updateImageData) => { 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? // 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 blob = new Blob([new Uint8Array(element.image_data.values()).buffer], { type: element.mime });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
createImageBitmap(blob).then((image) => { const image = await createImageBitmap(blob);
this.editor.instance.set_image_blob_url(element.path, url, image.width, image.height); this.editor.instance.set_image_blob_url(element.path, url, image.width, image.height);
}); });
}); });
});
// TODO(mfish33): Replace with initialization system Issue:#524 // TODO(mfish33): Replace with initialization system Issue:#524
// Get initial Document Bar // Get initial Document Bar
+23 -14
View File
@@ -42,11 +42,11 @@
class="layer-row" class="layer-row"
v-for="(listing, index) in layers" v-for="(listing, index) in layers"
:key="String(listing.entry.path.slice(-1))" :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"> <LayoutRow class="visibility">
<IconButton <IconButton
:action="(e) => (toggleLayerVisibility(listing.entry.path), e && e.stopPropagation())" :action="(e) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())"
:size="24" :size="24"
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'" :icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
:title="listing.entry.visible ? 'Visible' : 'Hidden'" :title="listing.entry.visible ? 'Visible' : 'Hidden'"
@@ -396,16 +396,16 @@ export default defineComponent({
markTopOffset(height: number): string { markTopOffset(height: number): string {
return `${height}px`; return `${height}px`;
}, },
async createEmptyFolder() { createEmptyFolder() {
this.editor.instance.create_empty_folder(); this.editor.instance.create_empty_folder();
}, },
async deleteSelectedLayers() { deleteSelectedLayers() {
this.editor.instance.delete_selected_layers(); this.editor.instance.delete_selected_layers();
}, },
async toggleLayerVisibility(path: BigUint64Array) { toggleLayerVisibility(path: BigUint64Array) {
this.editor.instance.toggle_layer_visibility(path); this.editor.instance.toggle_layer_visibility(path);
}, },
async handleExpandArrowClick(path: BigUint64Array) { handleExpandArrowClick(path: BigUint64Array) {
this.editor.instance.toggle_layer_expansion(path); this.editor.instance.toggle_layer_expansion(path);
}, },
onEditLayerName(listing: LayerListingInfo) { onEditLayerName(listing: LayerListingInfo) {
@@ -414,12 +414,12 @@ export default defineComponent({
this.draggable = false; this.draggable = false;
listing.editingName = true; 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(() => { this.$nextTick(() => {
(tree.querySelector("[data-text-input]:not([disabled])") as HTMLInputElement).select(); (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 // Eliminate duplicate events
if (!listing.editingName) return; if (!listing.editingName) return;
@@ -434,8 +434,7 @@ export default defineComponent({
listing.editingName = false; listing.editingName = false;
this.$nextTick(() => { this.$nextTick(() => {
const selection = window.getSelection(); window.getSelection()?.removeAllRanges();
if (selection) selection.removeAllRanges();
}); });
}, },
async setLayerBlendMode(newSelectedIndex: number) { async setLayerBlendMode(newSelectedIndex: number) {
@@ -536,7 +535,7 @@ export default defineComponent({
// Stop the drag from being shown as cancelled // Stop the drag from being shown as cancelled
event.preventDefault(); 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); this.draggingData = this.calculateDragIndex(tree, event.clientY);
}, },
async drop() { async drop() {
@@ -594,8 +593,8 @@ export default defineComponent({
mounted() { mounted() {
this.editor.dispatcher.subscribeJsMessage(DisplayDocumentLayerTreeStructure, (displayDocumentLayerTreeStructure) => { this.editor.dispatcher.subscribeJsMessage(DisplayDocumentLayerTreeStructure, (displayDocumentLayerTreeStructure) => {
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName); const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
const layerPathWithNameBeingEdited = layerWithNameBeingEdited && layerWithNameBeingEdited.entry.path; const layerPathWithNameBeingEdited = layerWithNameBeingEdited?.entry.path;
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited && layerPathWithNameBeingEdited.slice(-1)[0]; const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited?.slice(-1)[0];
const path = [] as bigint[]; const path = [] as bigint[];
this.layers = [] as LayerListingInfo[]; this.layers = [] as LayerListingInfo[];
@@ -606,9 +605,18 @@ export default defineComponent({
path.push(layerId); path.push(layerId);
const mapping = cache.get(path.toString()); 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); if (item.children.length >= 1) recurse(item, layers, cache);
path.pop(); path.pop();
}); });
}; };
@@ -626,6 +634,7 @@ export default defineComponent({
} else { } else {
this.layerCache.set(targetPath.toString(), targetLayer); this.layerCache.set(targetPath.toString(), targetLayer);
} }
this.setBlendModeForSelectedLayers(); this.setBlendModeForSelectedLayers();
this.setOpacityForSelectedLayers(); this.setOpacityForSelectedLayers();
}); });
@@ -1,5 +1,4 @@
<template> <template>
<div>{{ widgetData.name }}</div>
<div class="widget-row"> <div class="widget-row">
<template v-for="(component, index) in widgetData.widgets" :key="index"> <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` --> <!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
@@ -69,7 +69,7 @@ export default defineComponent({
handleClick() { handleClick() {
(this.$refs.floatingMenu as typeof FloatingMenu).setOpen(); (this.$refs.floatingMenu as typeof FloatingMenu).setOpen();
if (this.action) this.action(); this.action?.();
}, },
}, },
}); });
@@ -161,13 +161,13 @@ export default defineComponent({
}, },
onPointerDown(e: PointerEvent) { onPointerDown(e: PointerEvent) {
const saturationPicker = this.$refs.saturationPicker as typeof LayoutCol; 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 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 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; if (!(e.currentTarget instanceof HTMLElement) || !saturationPickerElement || !huePickerElement || !opacityPickerElement) return;
@@ -216,13 +216,13 @@ export default defineComponent({
}, },
updateRects() { updateRects() {
const saturationPicker = this.$refs.saturationPicker as typeof LayoutCol; 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 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 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; if (!saturationPickerElement || !huePickerElement || !opacityPickerElement) return;
@@ -9,7 +9,7 @@
<TextLabel :bold="true" class="heading">{{ dialog.state.heading }}</TextLabel> <TextLabel :bold="true" class="heading">{{ dialog.state.heading }}</TextLabel>
<TextLabel class="details">{{ dialog.state.details }}</TextLabel> <TextLabel class="details">{{ dialog.state.details }}</TextLabel>
<LayoutRow class="buttons-row" v-if="dialog.state.buttons.length > 0"> <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> </LayoutRow>
</LayoutCol> </LayoutCol>
</LayoutRow> </LayoutRow>
@@ -212,7 +212,7 @@ export default defineComponent({
const workspace = document.querySelector("[data-workspace]"); const workspace = document.querySelector("[data-workspace]");
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement; const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol; 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; const floatingMenu = this.$refs.floatingMenu as HTMLElement;
if (!workspace || !floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !floatingMenu) return; if (!workspace || !floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !floatingMenu) return;
@@ -298,30 +298,28 @@ export default defineComponent({
}, },
getWidth(callback: (width: number) => void) { getWidth(callback: (width: number) => void) {
this.$nextTick(() => { 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; const width = floatingMenuContent.clientWidth;
callback(width); callback(width);
}); });
}, },
disableMinWidth(callback: (minWidth: string) => void) { disableMinWidth(callback: (minWidth: string) => void) {
this.$nextTick(() => { 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; const initialMinWidth = floatingMenuContent.style.minWidth;
floatingMenuContent.style.minWidth = "0"; floatingMenuContent.style.minWidth = "0";
callback(initialMinWidth); callback(initialMinWidth);
}); });
}, },
enableMinWidth(minWidth: string) { 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; floatingMenuContent.style.minWidth = minWidth;
}, },
pointerMoveHandler(e: PointerEvent) { pointerMoveHandler(e: PointerEvent) {
const target = e.target as HTMLElement; const target = e.target as HTMLElement | undefined;
const pointerOverFloatingMenuKeepOpen = target && (target.closest("[data-hover-menu-keep-open]") as HTMLElement); const pointerOverFloatingMenuKeepOpen = target?.closest("[data-hover-menu-keep-open]") as HTMLElement | undefined;
const pointerOverFloatingMenuSpawner = target && (target.closest("[data-hover-menu-spawner]") as HTMLElement); const pointerOverFloatingMenuSpawner = target?.closest("[data-hover-menu-spawner]") as HTMLElement | undefined;
// TODO: Simplify the following expression when optional chaining is supported by the build system const pointerOverOwnFloatingMenuSpawner = pointerOverFloatingMenuSpawner?.parentElement?.contains(this.$refs.floatingMenu as HTMLElement);
const pointerOverOwnFloatingMenuSpawner =
pointerOverFloatingMenuSpawner && pointerOverFloatingMenuSpawner.parentElement && 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 // Swap this open floating menu with the one created by the floating menu spawner being hovered over
if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) { if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) {
this.setClosed(); this.setClosed();
@@ -372,10 +370,12 @@ export default defineComponent({
}, },
isPointerEventOutsideMenuElement(e: PointerEvent, element: HTMLElement, extraDistanceAllowed = 0): boolean { isPointerEventOutsideMenuElement(e: PointerEvent, element: HTMLElement, extraDistanceAllowed = 0): boolean {
const floatingMenuBounds = element.getBoundingClientRect(); const floatingMenuBounds = element.getBoundingClientRect();
if (floatingMenuBounds.left - e.clientX >= extraDistanceAllowed) return true; if (floatingMenuBounds.left - e.clientX >= extraDistanceAllowed) return true;
if (e.clientX - floatingMenuBounds.right >= extraDistanceAllowed) return true; if (e.clientX - floatingMenuBounds.right >= extraDistanceAllowed) return true;
if (floatingMenuBounds.top - e.clientY >= extraDistanceAllowed) return true; if (floatingMenuBounds.top - e.clientY >= extraDistanceAllowed) return true;
if (e.clientY - floatingMenuBounds.bottom >= extraDistanceAllowed) return true; if (e.clientY - floatingMenuBounds.bottom >= extraDistanceAllowed) return true;
return false; return false;
}, },
}, },
@@ -19,9 +19,9 @@
<span class="entry-label">{{ entry.label }}</span> <span class="entry-label">{{ entry.label }}</span>
<IconLabel v-if="entry.shortcutRequiresLock && !fullscreen.state.keyboardLocked" :icon="'Info'" :title="keyboardLockInfoMessage" /> <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> <div class="no-submenu-arrow" v-else></div>
<MenuList <MenuList
@@ -175,10 +175,10 @@ const MenuList = defineComponent({
scrollableY: { type: Boolean as PropType<boolean>, default: false }, scrollableY: { type: Boolean as PropType<boolean>, default: false },
}, },
methods: { methods: {
setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu) { setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu): void {
if (ref) menuEntry.ref = ref; if (ref) menuEntry.ref = ref;
}, },
handleEntryClick(menuEntry: MenuListEntry) { handleEntryClick(menuEntry: MenuListEntry): void {
(this.$refs.floatingMenu as typeof FloatingMenu).setClosed(); (this.$refs.floatingMenu as typeof FloatingMenu).setClosed();
if (menuEntry.checkbox) menuEntry.checked = !menuEntry.checked; if (menuEntry.checkbox) menuEntry.checked = !menuEntry.checked;
@@ -188,20 +188,20 @@ const MenuList = defineComponent({
this.$emit("update:activeEntry", menuEntry); this.$emit("update:activeEntry", menuEntry);
}, },
handleEntryPointerEnter(menuEntry: MenuListEntry) { handleEntryPointerEnter(menuEntry: MenuListEntry): void {
if (!menuEntry.children || !menuEntry.children.length) return; if (!menuEntry.children?.length) return;
if (menuEntry.ref) menuEntry.ref.setOpen(); if (menuEntry.ref) menuEntry.ref.setOpen();
else throw new Error("The menu bar floating menu has no associated ref"); else throw new Error("The menu bar floating menu has no associated ref");
}, },
handleEntryPointerLeave(menuEntry: MenuListEntry) { handleEntryPointerLeave(menuEntry: MenuListEntry): void {
if (!menuEntry.children || !menuEntry.children.length) return; if (!menuEntry.children?.length) return;
if (menuEntry.ref) menuEntry.ref.setClosed(); if (menuEntry.ref) menuEntry.ref.setClosed();
else throw new Error("The menu bar floating menu has no associated ref"); else throw new Error("The menu bar floating menu has no associated ref");
}, },
isMenuEntryOpen(menuEntry: MenuListEntry): boolean { 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(); if (menuEntry.ref) return menuEntry.ref.isOpen();
@@ -215,7 +215,7 @@ const MenuList = defineComponent({
}, },
isOpen(): boolean { isOpen(): boolean {
const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu; const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu;
return Boolean(floatingMenu && floatingMenu.isOpen()); return Boolean(floatingMenu?.isOpen());
}, },
async measureAndReportWidth() { async measureAndReportWidth() {
// API is experimental but supported in all browsers - https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready // 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: { computed: {
menuEntriesWithoutRefs(): MenuListEntryData[][] { menuEntriesWithoutRefs(): MenuListEntryData[][] {
const { menuEntries } = this; return this.menuEntries.map((entries) =>
return menuEntries.map((entries) =>
entries.map((entry) => { entries.map((entry) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ref, ...entryWithoutRef } = entry; const { ref, ...entryWithoutRef } = entry;
return entryWithoutRef; return entryWithoutRef;
}) })
@@ -112,34 +112,45 @@ export default defineComponent({
if (!this.disabled) (this.$refs.menuList as typeof MenuList).setOpen(); if (!this.disabled) (this.$refs.menuList as typeof MenuList).setOpen();
}, },
selectFont(newName: string) { selectFont(newName: string) {
if (this.isStyle) this.$emit("update:fontStyle", newName); let fontFamily;
else this.$emit("update:fontFamily", newName); let fontStyle;
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 fontFamily = this.isStyle ? this.fontFamily : newName;
const fontStyle = this.isStyle ? newName : getFontStyles(newName)[0];
const fontFile = getFontFile(fontFamily, fontStyle); const fontFile = getFontFile(fontFamily, fontStyle);
this.$emit("changeFont", { fontFamily, fontStyle, fontFile }); this.$emit("changeFont", { fontFamily, fontStyle, fontFile });
}
}, },
onWidthChanged(newWidth: number) { onWidthChanged(newWidth: number) {
this.minWidth = newWidth; this.minWidth = newWidth;
}, },
updateEntries(): { menuEntries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } { updateEntries(): { menuEntries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } {
let selectedIndex = -1; const choices = this.isStyle ? getFontStyles(this.fontFamily) : fontNames();
const menuEntries: SectionsOfMenuListEntries = [ const selectedChoice = this.isStyle ? this.fontStyle : this.fontFamily;
(this.isStyle ? getFontStyles(this.fontFamily) : fontNames()).map((name, index) => {
if (name === (this.isStyle ? this.fontStyle : this.fontFamily)) selectedIndex = index;
let selectedEntry: MenuListEntry | undefined;
const entries = choices.map((name) => {
const result: MenuListEntry = { const result: MenuListEntry = {
label: name, label: name,
action: (): void => this.selectFont(name), action: (): void => this.selectFont(name),
}; };
return result;
}),
];
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 }; return { menuEntries, activeEntry };
}, },
@@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="entry-container" v-for="(entry, index) in menuEntries" :key="index"> <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" /> <IconLabel :icon="entry.icon" v-if="entry.icon" />
<span v-if="entry.label">{{ entry.label }}</span> <span v-if="entry.label">{{ entry.label }}</span>
</div> </div>
@@ -152,7 +152,7 @@ export default defineComponent({
onIncrement(direction: IncrementDirection) { onIncrement(direction: IncrementDirection) {
if (Number.isNaN(this.value)) return; if (Number.isNaN(this.value)) return;
({ const actions = {
Add: (): void => { Add: (): void => {
const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor; const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor;
this.updateValue(this.value + directionAddend); this.updateValue(this.value + directionAddend);
@@ -162,11 +162,13 @@ export default defineComponent({
this.updateValue(this.value * directionMultiplier); this.updateValue(this.value * directionMultiplier);
}, },
Callback: (): void => { Callback: (): void => {
if (direction === "Increase" && this.incrementCallbackIncrease) this.incrementCallbackIncrease(); if (direction === "Increase") this.incrementCallbackIncrease?.();
if (direction === "Decrease" && this.incrementCallbackDecrease) this.incrementCallbackDecrease(); if (direction === "Decrease") this.incrementCallbackDecrease?.();
}, },
None: (): void => undefined, None: (): void => undefined,
}[this.incrementBehavior]()); };
const action = actions[this.incrementBehavior];
action();
}, },
updateValue(newValue: number) { updateValue(newValue: number) {
const invalid = Number.isNaN(newValue); const invalid = Number.isNaN(newValue);
@@ -91,7 +91,7 @@ export default defineComponent({
const index = this.entries.indexOf(menuEntry); const index = this.entries.indexOf(menuEntry);
this.$emit("update:selectedIndex", index); this.$emit("update:selectedIndex", index);
if (menuEntry.action) menuEntry.action(); menuEntry.action?.();
}, },
}, },
components: { components: {
@@ -27,14 +27,12 @@
</style> </style>
<script lang="ts"> <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"; import LayoutRow from "@/components/layout/LayoutRow.vue";
const icons: Record<IconName, { component: DefineComponent; size: IconSize }> = ICON_LIST;
export default defineComponent({ export default defineComponent({
props: { props: {
icon: { type: String as PropType<IconName>, required: true }, icon: { type: String as PropType<IconName>, required: true },
@@ -47,6 +45,7 @@ export default defineComponent({
}, },
components: { components: {
LayoutRow, LayoutRow,
// Import the components of all the icons
...Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component])), ...Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component])),
}, },
}); });
@@ -52,7 +52,7 @@ const MINOR_MARK_THICKNESS = 3;
export type RulerDirection = "Horizontal" | "Vertical"; 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 mod = (n: number, m: number): number => {
const remainder = n % m; const remainder = n % m;
return Math.floor(remainder >= 0 ? remainder : remainder + m); return Math.floor(remainder >= 0 ? remainder : remainder + m);
@@ -190,9 +190,7 @@ export default defineComponent({
this.dragging = false; this.dragging = false;
}, },
pointerMove(e: PointerEvent) { pointerMove(e: PointerEvent) {
if (this.dragging) { if (this.dragging) this.updateHandlePosition(e);
this.updateHandlePosition(e);
}
}, },
changePosition(difference: number) { changePosition(difference: number) {
this.clampHandlePosition(this.handlePosition + difference / this.trackLength()); this.clampHandlePosition(this.handlePosition + difference / this.trackLength());
+3 -3
View File
@@ -8,11 +8,11 @@
data-tab data-tab
v-for="(tabLabel, tabIndex) in tabLabels" v-for="(tabLabel, tabIndex) in tabLabels"
:key="tabIndex" :key="tabIndex"
@click="(e) => (e && e.stopPropagation(), clickAction && clickAction(tabIndex))" @click="(e) => (e?.stopPropagation(), clickAction?.(tabIndex))"
@click.middle="(e) => (e && e.stopPropagation(), closeAction && closeAction(tabIndex))" @click.middle="(e) => (e?.stopPropagation(), closeAction?.(tabIndex))"
> >
<span>{{ tabLabel }}</span> <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>
</LayoutRow> </LayoutRow>
<PopoverButton :icon="'VerticalEllipsis'"> <PopoverButton :icon="'VerticalEllipsis'">
@@ -7,18 +7,8 @@
:tabCloseButtons="true" :tabCloseButtons="true"
:tabMinWidths="true" :tabMinWidths="true"
:tabLabels="documents.state.documents.map((doc) => doc.displayName)" :tabLabels="documents.state.documents.map((doc) => doc.displayName)"
:clickAction=" :clickAction="(tabIndex) => editor.instance.select_document(documents.state.documents[tabIndex].id)"
(tabIndex) => { :closeAction="(tabIndex) => editor.instance.close_document_with_confirmation(documents.state.documents[tabIndex].id)"
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);
}
"
:tabActiveIndex="documents.state.activeDocumentIndex" :tabActiveIndex="documents.state.activeDocumentIndex"
ref="documentsPanel" ref="documentsPanel"
/> />
+20 -15
View File
@@ -1,6 +1,6 @@
import { plainToInstance } from "class-transformer"; 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"; import type { RustEditorInstance, WasmInstance } from "@/state/wasm-loader";
type JsMessageCallback<T extends JsMessage> = (messageData: T) => void; 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 handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WasmInstance, instance: RustEditorInstance): void => {
const messageConstructor = messageConstructors[messageType]; // 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
if (!messageConstructor) { const messageMaker = messageMakers[messageType];
if (!messageMaker) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error( console.error(
`Received a frontend message of type "${messageType}" but was not able to parse the data. ` + `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; 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 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" // 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 unwrappedMessageData = messageData[messageType] || {};
const isJsMessageConstructor = (fn: typeof messageConstructor): fn is typeof JsMessage => "jsMessageMarker" in fn; // 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.
let message: JsMessage; // 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 (isJsMessageConstructor(messageConstructor)) { // 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.
message = plainToInstance(messageConstructor, unwrappedMessageData); // The resulting `message` is an instance of a class that extends `JsMessage`.
} else { const message = messageIsClass ? plainToInstance(messageMaker, unwrappedMessageData) : messageMaker(unwrappedMessageData, wasm, instance);
message = messageConstructor(unwrappedMessageData, wasm, instance);
}
// It is ok to use constructor.name even with minification since it is used consistently with registerHandler // It is ok to use constructor.name even with minification since it is used consistently with registerHandler
const callback = subscriptions[message.constructor.name]; const callback = subscriptions[message.constructor.name];
if (callback && message) { // If we have constructed a valid message, then we try and execute the callback that the frontend has associated with this message.
callback(message); // The frontend should always have a callback for all messages, and so we display an error if one was not found.
} else if (message) { if (message) {
if (callback) callback(message);
// eslint-disable-next-line no-console // 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.`);
} }
}; };
+16 -16
View File
@@ -244,9 +244,9 @@ interface DataBuffer {
} }
export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayDocumentLayerTreeStructure { export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayDocumentLayerTreeStructure {
const { pointer, length } = input.data_buffer; const pointerNum = Number(input.data_buffer.pointer);
const pointerNum = Number(pointer); const lengthNum = Number(input.data_buffer.length);
const lengthNum = Number(length);
const wasmMemoryBuffer = wasm.wasm_memory().buffer; const wasmMemoryBuffer = wasm.wasm_memory().buffer;
// Decode the folder structure encoding // Decode the folder structure encoding
@@ -265,7 +265,7 @@ export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataB
for (let i = 0; i < structureSectionLength; i += 1) { for (let i = 0; i < structureSectionLength; i += 1) {
const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true); 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 // Set the MSB to 0 to clear the sign and then read the number as usual
const numberOfLayersAtThisDepth = msbSigned & ~msbMask; 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 // Unpacking rust types to more usable type in the frontend
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
function createWidgetLayout(widgetLayout: any[]): LayoutRow[] { function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
return widgetLayout.map((rowOrSection) => { return widgetLayout.map((rowOrSection): LayoutRow => {
if (rowOrSection.Row) { if (rowOrSection.Row) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const widgets = rowOrSection.Row.widgets.map((widgetHolder: any) => { const widgets = rowOrSection.Row.widgets.map((widgetHolder: any) => {
@@ -496,16 +496,16 @@ function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
return { widget_id, kind, props }; return { widget_id, kind, props };
}); });
return { const result: WidgetRow = { widgets };
name: rowOrSection.Row.name, return result;
widgets,
};
} }
if (rowOrSection.Section) { if (rowOrSection.Section) {
return { const { name } = rowOrSection.Section;
name: rowOrSection.Section.name, const layout = createWidgetLayout(rowOrSection.Section.layout);
layout: createWidgetLayout(rowOrSection.Section.layout),
}; const result: WidgetSection = { name, layout };
return result;
} }
throw new Error("Layout row type does not exist"); throw new Error("Layout row type does not exist");
@@ -524,12 +524,12 @@ export class TriggerTextCopy extends JsMessage {
export class TriggerViewportResize 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInstance) => JsMessage; type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInstance) => JsMessage;
type MessageMaker = typeof JsMessage | JSMessageFactory; type MessageMaker = typeof JsMessage | JSMessageFactory;
export const messageConstructors: Record<string, MessageMaker> = { export const messageMakers: Record<string, MessageMaker> = {
DisplayConfirmationToCloseAllDocuments, DisplayConfirmationToCloseAllDocuments,
DisplayConfirmationToCloseDocument, DisplayConfirmationToCloseDocument,
DisplayDialogAboutGraphite, DisplayDialogAboutGraphite,
@@ -568,4 +568,4 @@ export const messageConstructors: Record<string, MessageMaker> = {
UpdateToolOptionsLayout, UpdateToolOptionsLayout,
UpdateWorkingColors, UpdateWorkingColors,
} as const; } as const;
export type JsMessageType = keyof typeof messageConstructors; export type JsMessageType = keyof typeof messageMakers;
+1 -3
View File
@@ -86,7 +86,5 @@ export function createAutoSaveManager(editor: EditorState, documents: DocumentsS
// On creation // On creation
openAutoSavedDocuments(); openAutoSavedDocuments();
return { return { openAutoSavedDocuments };
openAutoSavedDocuments,
};
} }
+26 -19
View File
@@ -42,9 +42,14 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
if (!key) return false; if (!key) return false;
// Don't redirect user input from text entry into HTML elements // Don't redirect user input from text entry into HTML elements
const { target } = e; if (
if (key !== "escape" && !(key === "enter" && e.ctrlKey) && target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable)) key !== "escape" &&
!(key === "enter" && e.ctrlKey) &&
e.target instanceof HTMLElement &&
(e.target.nodeName === "INPUT" || e.target.nodeName === "TEXTAREA" || e.target.isContentEditable)
) {
return false; return false;
}
// Don't redirect paste // Don't redirect paste
if (key === "v" && e.ctrlKey) return false; if (key === "v" && e.ctrlKey) return false;
@@ -124,9 +129,10 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
e.stopPropagation(); e.stopPropagation();
} }
if (textInput && !inTextInput) { if (!inTextInput) {
editor.instance.on_change_text(textInputCleanup(textInput.innerText)); if (textInput) editor.instance.on_change_text(textInputCleanup(textInput.innerText));
} else if (inCanvas && !inTextInput) viewportPointerInteractionOngoing = true; else if (inCanvas) viewportPointerInteractionOngoing = true;
}
if (viewportPointerInteractionOngoing) { if (viewportPointerInteractionOngoing) {
const modifiers = makeModifiersBitfield(e); const modifiers = makeModifiersBitfield(e);
@@ -164,6 +170,8 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
const { target } = e; const { target } = e;
const inCanvas = target instanceof Element && target.closest("[data-canvas]"); 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]"); const horizontalScrollableElement = target instanceof Element && target.closest("[data-scrollable-x]");
if (horizontalScrollableElement && e.deltaY !== 0) { if (horizontalScrollableElement && e.deltaY !== 0) {
horizontalScrollableElement.scrollTo(horizontalScrollableElement.scrollLeft + 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(); const file = item.getAsFile();
if (file && file.type.startsWith("image")) { if (file?.type.startsWith("image")) {
file.arrayBuffer().then((buffer): void => { file.arrayBuffer().then((buffer): void => {
const u8Array = new Uint8Array(buffer); const u8Array = new Uint8Array(buffer);
@@ -252,9 +260,7 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
addListeners(); addListeners();
onWindowResize(container); onWindowResize(container);
return { return { removeListeners };
removeListeners,
};
} }
export type InputManager = ReturnType<typeof createInputManager>; export type InputManager = ReturnType<typeof createInputManager>;
@@ -296,7 +302,12 @@ function keyCodeToKey(code: string): string | null {
if (code.match(/^F[1-9]|F1[0-9]|F20$/)) return code.replace("F", "").toLowerCase(); if (code.match(/^F[1-9]|F1[0-9]|F20$/)) return code.replace("F", "").toLowerCase();
// Other characters // Other characters
const mapping: Record<string, string> = { if (MAPPING[code]) return MAPPING[code];
return null;
}
const MAPPING: Record<string, string> = {
BracketLeft: "[", BracketLeft: "[",
BracketRight: "]", BracketRight: "]",
Backslash: "\\", Backslash: "\\",
@@ -314,13 +325,12 @@ function keyCodeToKey(code: string): string | null {
NumpadAdd: "+", NumpadAdd: "+",
NumpadDecimal: ".", NumpadDecimal: ".",
}; };
if (code in mapping) return mapping[code];
return null;
}
function isKeyPrintable(key: string): boolean { function isKeyPrintable(key: string): boolean {
const allPrintableKeys: string[] = [ return !ALL_PRINTABLE_KEYS.has(key);
}
const ALL_PRINTABLE_KEYS = new Set([
// Modifier // Modifier
"Alt", "Alt",
"AltGraph", "AltGraph",
@@ -624,7 +634,4 @@ function isKeyPrintable(key: string): boolean {
"VideoModeNext", "VideoModeNext",
"Wink", "Wink",
"ZoomToggle", "ZoomToggle",
]; ]);
return !allPrintableKeys.includes(key);
}
+3 -7
View File
@@ -30,10 +30,7 @@ export function createDialogState(editor: EditorState) {
const submitDialog = (): void => { const submitDialog = (): void => {
const firstEmphasizedButton = state.buttons.find((button) => button.props.emphasized && button.callback); const firstEmphasizedButton = state.buttons.find((button) => button.props.emphasized && button.callback);
if (firstEmphasizedButton) { firstEmphasizedButton?.callback?.();
// If statement satisfies TypeScript
if (firstEmphasizedButton.callback) firstEmphasizedButton.callback();
}
}; };
const dialogIsVisible = (): boolean => state.visible; 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"), callback: async () => window.open(`https://github.com/GraphiteEditor/Graphite/issues/${issueNumber}`, "_blank"),
props: { label: `Issue #${issueNumber}`, minWidth: 96 }, props: { label: `Issue #${issueNumber}`, minWidth: 96 },
}; };
const buttons = [okButton]; const buttons = issueNumber ? [okButton, issueButton] : [okButton];
if (issueNumber) buttons.push(issueButton);
createDialog("Warning", "Coming soon", details, buttons); createDialog("Warning", "Coming soon", details, buttons);
}; };
@@ -65,7 +61,7 @@ export function createDialogState(editor: EditorState) {
const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" }) const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
.formatToParts(new Date()) .formatToParts(new Date())
.find((part) => part.type === "timeZoneName"); .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); const hash = (process.env.VUE_APP_COMMIT_HASH || "").substring(0, 12);
+4 -2
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 // `wasmImport` starts uninitialized until `initWasm()` is called in `main.ts` before the Vue app is created
let wasmImport: WasmInstance | null = null; let wasmImport: WasmInstance | null = null;
export async function initWasm(): Promise<void> { export async function initWasm(): Promise<void> {
// Skip if the wasm module is already initialized
if (wasmImport !== null) return; if (wasmImport !== null) return;
// Separating in two lines satisfies typescript when used below // Separating in two lines satisfies typescript when used below
const importedWasm = await import("@/../wasm/pkg").then(panicProxy); const importedWasm = await import("@/../wasm/pkg").then(panicProxy);
wasmImport = importedWasm; 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)); const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
importedWasm.set_random_seed(randomSeed); importedWasm.set_random_seed(randomSeed);
} }
// This works by proxying every function call wrapping a try-catch block to filter out redundant and confusing // This works by proxying every function call and wrapping a try-catch block to filter out redundant and confusing
// `RuntimeError: unreachable` exceptions sent to the console // `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 { function panicProxy<T extends object>(module: T): T {
const proxyHandler = { const proxyHandler = {
get(target: T, propKey: string | symbol, receiver: unknown): unknown { get(target: T, propKey: string | symbol, receiver: unknown): unknown {
+2 -1
View File
@@ -21,12 +21,13 @@ export async function upload(acceptedEextensions: string): Promise<{ filename: s
element.addEventListener( element.addEventListener(
"change", "change",
async () => { async () => {
if (!element.files || !element.files.length) return; if (element.files?.length) {
const file = element.files[0]; const file = element.files[0];
const filename = file.name; const filename = file.name;
const content = await file.text(); const content = await file.text();
resolve({ filename, content }); resolve({ filename, content });
}
}, },
{ capture: false, once: true } { capture: false, once: true }
); );
+10 -11
View File
@@ -23,12 +23,14 @@ fetch(fontListAPI)
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
const loadedFonts = json.items as { family: string; variants: string[]; files: { [name: string]: string } }[]; const loadedFonts = json.items as { family: string; variants: string[]; files: { [name: string]: string } }[];
fontList = loadedFonts.map((font) => { fontList = loadedFonts.map((font) => {
const { family } = font; const { family } = font;
const variants = font.variants.map(formatFontStyleName); const variants = font.variants.map(formatFontStyleName);
const files = new Map(font.variants.map((x) => [formatFontStyleName(x), font.files[x]])); const files = new Map(font.variants.map((x) => [formatFontStyleName(x), font.files[x]]));
return { family, variants, files }; return { family, variants, files };
}); });
loadDefaultFont(); loadDefaultFont();
}); });
@@ -48,16 +50,13 @@ function formatFontStyleName(fontStyle: string): string {
return `${weightName}${isItalic ? " Italic" : ""} (${weight})`; return `${weightName}${isItalic ? " Italic" : ""} (${weight})`;
} }
export function loadDefaultFont(): void { export async function loadDefaultFont(): Promise<void> {
const font = getFontFile("Merriweather", "Normal (400)"); const font = getFontFile("Merriweather", "Normal (400)");
if (!font) return;
if (font) { const response = await fetch(font);
fetch(font) const responseBuffer = await response.arrayBuffer();
.then((response) => response.arrayBuffer()) loadDefaultFontCallback?.(font, new Uint8Array(responseBuffer));
.then((response) => {
if (loadDefaultFontCallback) loadDefaultFontCallback(font, new Uint8Array(response));
});
}
} }
export function setLoadDefaultFontCallback(callback: fontCallbackType): void { export function setLoadDefaultFontCallback(callback: fontCallbackType): void {
@@ -71,11 +70,11 @@ export function fontNames(): string[] {
export function getFontStyles(fontFamily: string): string[] { export function getFontStyles(fontFamily: string): string[] {
const font = fontList.find((value) => value.family === fontFamily); const font = fontList.find((value) => value.family === fontFamily);
return font ? font.variants : []; return font?.variants || [];
} }
export function getFontFile(fontFamily: string, fontStyle: string): string | undefined { export function getFontFile(fontFamily: string, fontStyle: string): string | undefined {
const font = fontList.find((value) => value.family === fontFamily); const font = fontList.find((value) => value.family === fontFamily);
const fontFile = font && font.files.get(fontStyle); const fontFile = font?.files.get(fontStyle);
return fontFile && fontFile.replace("http://", "https://"); return fontFile?.replace("http://", "https://");
} }
+110 -101
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 VectorSplineTool from "@/../assets/24px-two-tone/vector-spline-tool.svg";
import VectorTextTool from "@/../assets/24px-two-tone/vector-text-tool.svg"; import VectorTextTool from "@/../assets/24px-two-tone/vector-text-tool.svg";
export type IconName = keyof typeof ICON_LIST; const ICON_LIST = {
export type IconSize = 12 | 16 | 24; 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; AlignBottom: { component: AlignBottom, size: 16 },
const size16: IconSize = 16; AlignHorizontalCenter: { component: AlignHorizontalCenter, size: 16 },
const size24: IconSize = 24; 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 = { MouseHintDrag: { component: MouseHintDrag, size: 16 },
Checkmark: { component: Checkmark, size: size12 }, MouseHintLmbDrag: { component: MouseHintLmbDrag, size: 16 },
CloseX: { component: CloseX, size: size12 }, MouseHintLmb: { component: MouseHintLmb, size: 16 },
DropdownArrow: { component: DropdownArrow, size: size12 }, MouseHintMmbDrag: { component: MouseHintMmbDrag, size: 16 },
FullscreenEnter: { component: FullscreenEnter, size: size12 }, MouseHintMmb: { component: MouseHintMmb, size: 16 },
FullscreenExit: { component: FullscreenExit, size: size12 }, MouseHintNone: { component: MouseHintNone, size: 16 },
Grid: { component: Grid, size: size12 }, MouseHintRmbDrag: { component: MouseHintRmbDrag, size: 16 },
Info: { component: Info, size: size12 }, MouseHintRmb: { component: MouseHintRmb, size: 16 },
KeyboardArrowDown: { component: KeyboardArrowDown, size: size12 }, MouseHintScrollDown: { component: MouseHintScrollDown, size: 16 },
KeyboardArrowLeft: { component: KeyboardArrowLeft, size: size12 }, MouseHintScrollUp: { component: MouseHintScrollUp, size: 16 },
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 },
AlignBottom: { component: AlignBottom, size: size16 }, GeneralArtboardTool: { component: GeneralArtboardTool, size: 24 },
AlignHorizontalCenter: { component: AlignHorizontalCenter, size: size16 }, GeneralEyedropperTool: { component: GeneralEyedropperTool, size: 24 },
AlignLeft: { component: AlignLeft, size: size16 }, GeneralNavigateTool: { component: GeneralNavigateTool, size: 24 },
AlignRight: { component: AlignRight, size: size16 }, GeneralSelectTool: { component: GeneralSelectTool, size: 24 },
AlignTop: { component: AlignTop, size: size16 }, GeneralFillTool: { component: GeneralFillTool, size: 24 },
AlignVerticalCenter: { component: AlignVerticalCenter, size: size16 }, GeneralGradientTool: { component: GeneralGradientTool, size: 24 },
BooleanDifference: { component: BooleanDifference, size: size16 }, RasterBrushTool: { component: RasterBrushTool, size: 24 },
BooleanIntersect: { component: BooleanIntersect, size: size16 }, RasterCloneTool: { component: RasterCloneTool, size: 24 },
BooleanSubtractBack: { component: BooleanSubtractBack, size: size16 }, RasterDetailTool: { component: RasterDetailTool, size: 24 },
BooleanSubtractFront: { component: BooleanSubtractFront, size: size16 }, RasterHealTool: { component: RasterHealTool, size: 24 },
BooleanUnion: { component: BooleanUnion, size: size16 }, RasterPatchTool: { component: RasterPatchTool, size: 24 },
Copy: { component: Copy, size: size16 }, RasterRelightTool: { component: RasterRelightTool, size: 24 },
EyeHidden: { component: EyeHidden, size: size16 }, VectorEllipseTool: { component: VectorEllipseTool, size: 24 },
EyeVisible: { component: EyeVisible, size: size16 }, VectorFreehandTool: { component: VectorFreehandTool, size: 24 },
File: { component: File, size: size16 }, VectorLineTool: { component: VectorLineTool, size: 24 },
FlipHorizontal: { component: FlipHorizontal, size: size16 }, VectorPathTool: { component: VectorPathTool, size: 24 },
FlipVertical: { component: FlipVertical, size: size16 }, VectorPenTool: { component: VectorPenTool, size: 24 },
GraphiteLogo: { component: GraphiteLogo, size: size16 }, VectorRectangleTool: { component: VectorRectangleTool, size: 24 },
NodeArtboard: { component: NodeArtboard, size: size16 }, VectorShapeTool: { component: VectorShapeTool, size: 24 },
NodeFolder: { component: NodeFolder, size: size16 }, VectorSplineTool: { component: VectorSplineTool, size: 24 },
NodeImage: { component: NodeImage, size: size16 }, VectorTextTool: { component: VectorTextTool, size: 24 },
NodeShape: { component: NodeShape, size: size16 }, } as const;
NodeText: { component: NodeText, size: size16 }, export const icons: IconDefinitionType<typeof ICON_LIST> = ICON_LIST;
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 },
MouseHintDrag: { component: MouseHintDrag, size: size16 }, export type IconName = keyof typeof icons;
MouseHintLmbDrag: { component: MouseHintLmbDrag, size: size16 }, export type IconSize = 12 | 16 | 24 | 32;
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 },
GeneralArtboardTool: { component: GeneralArtboardTool, 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
GeneralEyedropperTool: { component: GeneralEyedropperTool, size: size24 }, // 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.
GeneralNavigateTool: { component: GeneralNavigateTool, size: size24 }, // 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`.
GeneralSelectTool: { component: GeneralSelectTool, size: size24 }, // 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
GeneralFillTool: { component: GeneralFillTool, size: size24 }, // 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
GeneralGradientTool: { component: GeneralGradientTool, size: size24 }, // 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
RasterBrushTool: { component: RasterBrushTool, size: size24 }, // 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.
RasterCloneTool: { component: RasterCloneTool, size: size24 }, // Based on https://stackoverflow.com/a/64119715/775283
RasterDetailTool: { component: RasterDetailTool, size: size24 }, type IconDefinition = { component: string; size: IconSize };
RasterHealTool: { component: RasterHealTool, size: size24 }, type EvaluateType<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
RasterPatchTool: { component: RasterPatchTool, size: size24 }, type IconDefinitionType<T extends Record<string, IconDefinition>> = EvaluateType<{ [key in keyof T]: IconDefinition }>;
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 },
};