mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-03 21:08:18 +00:00
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:
parent
92ee3bbad3
commit
07736a9fca
28 changed files with 640 additions and 622 deletions
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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` -->
|
||||
|
|
|
@ -69,7 +69,7 @@ export default defineComponent({
|
|||
handleClick() {
|
||||
(this.$refs.floatingMenu as typeof FloatingMenu).setOpen();
|
||||
|
||||
if (this.action) this.action();
|
||||
this.action?.();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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])),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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'">
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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.`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -86,7 +86,5 @@ export function createAutoSaveManager(editor: EditorState, documents: DocumentsS
|
|||
// On creation
|
||||
openAutoSavedDocuments();
|
||||
|
||||
return {
|
||||
openAutoSavedDocuments,
|
||||
};
|
||||
return { openAutoSavedDocuments };
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
]);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
|
|
|
@ -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://");
|
||||
}
|
||||
|
|
|
@ -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 }>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue