Add font menu previews and virtual scrolling (#650)
* Keyboard menu navigation * Fix dropdown keyboard navigation * Fix merge error * Some code review * Interactive dropdowns * Query by data attr not class name * Add locking behaviour * Add font prieviews * Remove blank line in css * Use default for interactive in struct * Use menulist for fontinput * Polish * Rename state -> manager * Code review * Cleanup fontinput * More cleanup * Make fonts.ts an empty state * Fix regression Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
a26a0ddfcf
commit
3a30cdbb70
|
|
@ -681,6 +681,7 @@ impl DocumentMessageHandler {
|
||||||
]],
|
]],
|
||||||
selected_index: Some(self.document_mode as u32),
|
selected_index: Some(self.document_mode as u32),
|
||||||
draw_icon: true,
|
draw_icon: true,
|
||||||
|
interactive: false, // TODO: set to true when dialogs are not spawned
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::Separator(Separator {
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
|
|
||||||
|
|
@ -6,44 +6,57 @@
|
||||||
:type="'Dropdown'"
|
:type="'Dropdown'"
|
||||||
:windowEdgeMargin="0"
|
:windowEdgeMargin="0"
|
||||||
:escapeCloses="false"
|
:escapeCloses="false"
|
||||||
v-bind="{ direction, scrollableY, minWidth }"
|
v-bind="{ direction, scrollableY: scrollableY && virtualScrollingEntryHeight === 0, minWidth }"
|
||||||
ref="floatingMenu"
|
ref="floatingMenu"
|
||||||
data-hover-menu-keep-open
|
data-hover-menu-keep-open
|
||||||
>
|
>
|
||||||
<template v-for="(section, sectionIndex) in entries" :key="sectionIndex">
|
<!-- If we put the scrollableY on the layoutcol for non-font dropdowns then for some reason it always creates a tiny scrollbar.
|
||||||
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
|
However when we are using the virtual scrolling then we need the layoutcol to be scrolling so we can bind the events without using $refs. -->
|
||||||
|
<LayoutCol ref="scroller" :scrollableY="scrollableY && virtualScrollingEntryHeight !== 0" @scroll="onScroll" :style="{ minWidth: virtualScrollingEntryHeight ? `${minWidth}px` : `inherit` }">
|
||||||
|
<LayoutRow v-if="virtualScrollingEntryHeight" class="scroll-spacer" :style="{ height: `${virtualScrollingStartIndex * virtualScrollingEntryHeight}px` }"></LayoutRow>
|
||||||
|
<template v-for="(section, sectionIndex) in entries" :key="sectionIndex">
|
||||||
|
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
|
||||||
|
<LayoutRow
|
||||||
|
v-for="(entry, entryIndex) in virtualScrollingEntryHeight ? section.slice(virtualScrollingStartIndex, virtualScrollingEndIndex) : section"
|
||||||
|
:key="entryIndex + (virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0)"
|
||||||
|
class="row"
|
||||||
|
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label }"
|
||||||
|
:style="{ height: virtualScrollingEntryHeight || '20px' }"
|
||||||
|
@click="() => onEntryClick(entry)"
|
||||||
|
@pointerenter="() => onEntryPointerEnter(entry)"
|
||||||
|
@pointerleave="() => onEntryPointerLeave(entry)"
|
||||||
|
>
|
||||||
|
<CheckboxInput v-if="entry.checkbox" v-model:checked="entry.checked" :outlineStyle="true" :disableTabIndex="true" class="entry-checkbox" />
|
||||||
|
<IconLabel v-else-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
|
||||||
|
<div v-else-if="drawIcon" class="no-icon"></div>
|
||||||
|
|
||||||
|
<link v-if="entry.font" rel="stylesheet" :href="entry.font?.toString()" />
|
||||||
|
|
||||||
|
<span class="entry-label" :style="{ fontFamily: `${!entry.font ? 'inherit' : entry.value}` }">{{ entry.label }}</span>
|
||||||
|
|
||||||
|
<IconLabel v-if="entry.shortcutRequiresLock && !fullscreen.state.keyboardLocked" :icon="'Info'" :title="keyboardLockInfoMessage" />
|
||||||
|
<UserInputLabel v-else-if="entry.shortcut?.length" :inputKeys="[entry.shortcut]" />
|
||||||
|
|
||||||
|
<div class="submenu-arrow" v-if="entry.children?.length"></div>
|
||||||
|
<div class="no-submenu-arrow" v-else></div>
|
||||||
|
|
||||||
|
<MenuList
|
||||||
|
v-if="entry.children"
|
||||||
|
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
|
||||||
|
:open="entry.ref?.open || false"
|
||||||
|
:direction="'TopRight'"
|
||||||
|
:entries="entry.children"
|
||||||
|
v-bind="{ defaultAction, minWidth, drawIcon, scrollableY }"
|
||||||
|
:ref="(ref: typeof FloatingMenu) => ref && (entry.ref = ref)"
|
||||||
|
/>
|
||||||
|
</LayoutRow>
|
||||||
|
</template>
|
||||||
<LayoutRow
|
<LayoutRow
|
||||||
v-for="(entry, entryIndex) in section"
|
v-if="virtualScrollingEntryHeight"
|
||||||
:key="entryIndex"
|
class="scroll-spacer"
|
||||||
class="row"
|
:style="{ height: `${virtualScrollingTotalHeight - virtualScrollingEndIndex * virtualScrollingEntryHeight}px` }"
|
||||||
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label }"
|
></LayoutRow>
|
||||||
@click="() => onEntryClick(entry)"
|
</LayoutCol>
|
||||||
@pointerenter="() => onEntryPointerEnter(entry)"
|
|
||||||
@pointerleave="() => onEntryPointerLeave(entry)"
|
|
||||||
>
|
|
||||||
<CheckboxInput v-if="entry.checkbox" v-model:checked="entry.checked" :outlineStyle="true" :disableTabIndex="true" class="entry-checkbox" />
|
|
||||||
<IconLabel v-else-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
|
|
||||||
<div v-else-if="drawIcon" class="no-icon"></div>
|
|
||||||
|
|
||||||
<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?.length" :inputKeys="[entry.shortcut]" />
|
|
||||||
|
|
||||||
<div class="submenu-arrow" v-if="entry.children?.length"></div>
|
|
||||||
<div class="no-submenu-arrow" v-else></div>
|
|
||||||
|
|
||||||
<MenuList
|
|
||||||
v-if="entry.children"
|
|
||||||
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
|
|
||||||
:open="entry.ref?.open || false"
|
|
||||||
:direction="'TopRight'"
|
|
||||||
:entries="entry.children"
|
|
||||||
v-bind="{ defaultAction, minWidth, drawIcon, scrollableY }"
|
|
||||||
:ref="(ref: typeof FloatingMenu) => ref && (entry.ref = ref)"
|
|
||||||
/>
|
|
||||||
</LayoutRow>
|
|
||||||
</template>
|
|
||||||
</FloatingMenu>
|
</FloatingMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -52,6 +65,10 @@
|
||||||
.floating-menu-container .floating-menu-content {
|
.floating-menu-container .floating-menu-content {
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.scroll-spacer {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -145,6 +162,7 @@ import { defineComponent, PropType } from "vue";
|
||||||
import { IconName } from "@/utility-functions/icons";
|
import { IconName } from "@/utility-functions/icons";
|
||||||
|
|
||||||
import FloatingMenu, { MenuDirection } from "@/components/floating-menus/FloatingMenu.vue";
|
import FloatingMenu, { MenuDirection } from "@/components/floating-menus/FloatingMenu.vue";
|
||||||
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
|
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
|
@ -158,6 +176,7 @@ interface MenuListEntryData<Value = string> {
|
||||||
value?: Value;
|
value?: Value;
|
||||||
label?: string;
|
label?: string;
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
|
font?: URL;
|
||||||
checkbox?: boolean;
|
checkbox?: boolean;
|
||||||
shortcut?: string[];
|
shortcut?: string[];
|
||||||
shortcutRequiresLock?: boolean;
|
shortcutRequiresLock?: boolean;
|
||||||
|
|
@ -182,6 +201,7 @@ const MenuList = defineComponent({
|
||||||
drawIcon: { type: Boolean as PropType<boolean>, default: false },
|
drawIcon: { type: Boolean as PropType<boolean>, default: false },
|
||||||
interactive: { type: Boolean as PropType<boolean>, default: false },
|
interactive: { type: Boolean as PropType<boolean>, default: false },
|
||||||
scrollableY: { type: Boolean as PropType<boolean>, default: false },
|
scrollableY: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
virtualScrollingEntryHeight: { type: Number as PropType<number>, default: 0 },
|
||||||
defaultAction: { type: Function as PropType<() => void>, required: false },
|
defaultAction: { type: Function as PropType<() => void>, required: false },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -189,6 +209,7 @@ const MenuList = defineComponent({
|
||||||
isOpen: this.open,
|
isOpen: this.open,
|
||||||
keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
|
keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
|
||||||
highlighted: this.activeEntry as MenuListEntry | undefined,
|
highlighted: this.activeEntry as MenuListEntry | undefined,
|
||||||
|
virtualScrollingEntriesStart: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -326,6 +347,10 @@ const MenuList = defineComponent({
|
||||||
// Interactive menus should keep the active entry the same as the highlighted one
|
// Interactive menus should keep the active entry the same as the highlighted one
|
||||||
if (this.interactive && newHighlight?.value !== this.activeEntry?.value) this.$emit("update:activeEntry", newHighlight);
|
if (this.interactive && newHighlight?.value !== this.activeEntry?.value) this.$emit("update:activeEntry", newHighlight);
|
||||||
},
|
},
|
||||||
|
onScroll(e: Event) {
|
||||||
|
if (!this.virtualScrollingEntryHeight) return;
|
||||||
|
this.virtualScrollingEntriesStart = (e.target as HTMLElement)?.scrollTop || 0;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
entriesWithoutRefs(): MenuListEntryData[][] {
|
entriesWithoutRefs(): MenuListEntryData[][] {
|
||||||
|
|
@ -336,6 +361,15 @@ const MenuList = defineComponent({
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
virtualScrollingTotalHeight() {
|
||||||
|
return this.entries[0].length * this.virtualScrollingEntryHeight;
|
||||||
|
},
|
||||||
|
virtualScrollingStartIndex() {
|
||||||
|
return Math.floor(this.virtualScrollingEntriesStart / this.virtualScrollingEntryHeight);
|
||||||
|
},
|
||||||
|
virtualScrollingEndIndex() {
|
||||||
|
return Math.min(this.entries[0].length, this.virtualScrollingStartIndex + 1 + 400 / this.virtualScrollingEntryHeight);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
FloatingMenu,
|
FloatingMenu,
|
||||||
|
|
@ -344,6 +378,7 @@ const MenuList = defineComponent({
|
||||||
CheckboxInput,
|
CheckboxInput,
|
||||||
UserInputLabel,
|
UserInputLabel,
|
||||||
LayoutRow,
|
LayoutRow,
|
||||||
|
LayoutCol,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export default MenuList;
|
export default MenuList;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="font-input">
|
<LayoutRow class="font-input">
|
||||||
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" @click="() => !disabled && (open = true)" data-hover-menu-spawner>
|
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" tabindex="0" @click="toggleOpen" @keydown="keydown" data-hover-menu-spawner>
|
||||||
<span>{{ activeEntry?.label || "" }}</span>
|
<span>{{ activeEntry?.value || "" }}</span>
|
||||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<MenuList
|
<MenuList
|
||||||
|
ref="menulist"
|
||||||
v-model:activeEntry="activeEntry"
|
v-model:activeEntry="activeEntry"
|
||||||
v-model:open="open"
|
v-model:open="open"
|
||||||
@naturalWidth="(newNaturalWidth: number) => (minWidth = newNaturalWidth)"
|
:entries="[entries]"
|
||||||
:entries="entries"
|
:minWidth="isStyle ? 0 : minWidth"
|
||||||
:direction="'Bottom'"
|
:virtualScrollingEntryHeight="isStyle ? 0 : 20"
|
||||||
:scrollableY="true"
|
:scrollableY="true"
|
||||||
/>
|
@naturalWidth="(newNaturalWidth: number) => (isStyle && (minWidth = newNaturalWidth))"
|
||||||
|
></MenuList>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -26,21 +28,12 @@
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
.dropdown-icon {
|
|
||||||
margin: 4px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-icon + span {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-arrow {
|
.dropdown-arrow {
|
||||||
margin: 6px 2px;
|
margin: 6px 2px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
@ -53,10 +46,6 @@
|
||||||
span {
|
span {
|
||||||
color: var(--color-f-white);
|
color: var(--color-f-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--color-f-white);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
|
|
@ -69,23 +58,23 @@
|
||||||
span {
|
span {
|
||||||
color: var(--color-8-uppergray);
|
color: var(--color-8-uppergray);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--color-8-uppergray);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-list .floating-menu-container .floating-menu-content {
|
.menu-list .floating-menu-container .floating-menu-content {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from "vue";
|
import { defineComponent, nextTick, PropType } from "vue";
|
||||||
|
|
||||||
import MenuList, { MenuListEntry, SectionsOfMenuListEntries } from "@/components/floating-menus/MenuList.vue";
|
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
|
||||||
|
import MenuList, { MenuListEntry } from "@/components/floating-menus/MenuList.vue";
|
||||||
|
|
||||||
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
|
||||||
|
|
@ -101,17 +90,42 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
open: false,
|
open: false,
|
||||||
minWidth: 0,
|
entries: [] as MenuListEntry[],
|
||||||
entries: [] as SectionsOfMenuListEntries,
|
activeEntry: undefined as MenuListEntry | undefined,
|
||||||
activeEntry: undefined as undefined | MenuListEntry,
|
highlighted: undefined as MenuListEntry | undefined,
|
||||||
|
entriesStart: 0,
|
||||||
|
minWidth: this.isStyle ? 0 : 300,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const { entries, activeEntry } = await this.updateEntries();
|
this.entries = await this.getEntries();
|
||||||
this.entries = entries;
|
this.activeEntry = this.getActiveEntry(this.entries);
|
||||||
this.activeEntry = activeEntry;
|
this.highlighted = this.activeEntry;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
floatingMenu() {
|
||||||
|
return this.$refs.floatingMenu as typeof FloatingMenu;
|
||||||
|
},
|
||||||
|
scroller() {
|
||||||
|
return ((this.$refs.menulist as typeof MenuList).$refs.scroller as typeof LayoutCol)?.$el as HTMLElement;
|
||||||
|
},
|
||||||
|
async setOpen() {
|
||||||
|
this.open = true;
|
||||||
|
// Scroll to the active entry (the scroller div does not yet exist so we must wait for vue to render)
|
||||||
|
await nextTick();
|
||||||
|
if (this.activeEntry) {
|
||||||
|
const index = this.entries.indexOf(this.activeEntry);
|
||||||
|
this.scroller()?.scrollTo(0, Math.max(0, index * 20 - 190));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleOpen() {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this.open = !this.open;
|
||||||
|
if (this.open) this.setOpen();
|
||||||
|
},
|
||||||
|
keydown(e: KeyboardEvent) {
|
||||||
|
(this.$refs.menulist as typeof MenuList).keydown(e, false);
|
||||||
|
},
|
||||||
async selectFont(newName: string): Promise<void> {
|
async selectFont(newName: string): Promise<void> {
|
||||||
let fontFamily;
|
let fontFamily;
|
||||||
let fontStyle;
|
let fontStyle;
|
||||||
|
|
@ -125,50 +139,43 @@ export default defineComponent({
|
||||||
this.$emit("update:fontFamily", newName);
|
this.$emit("update:fontFamily", newName);
|
||||||
|
|
||||||
fontFamily = newName;
|
fontFamily = newName;
|
||||||
fontStyle = (await this.fonts.getFontStyles(newName))[0];
|
fontStyle = "Normal (400)";
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontFileUrl = await this.fonts.getFontFileUrl(fontFamily, fontStyle);
|
const fontFileUrl = await this.fonts.getFontFileUrl(fontFamily, fontStyle);
|
||||||
this.$emit("changeFont", { fontFamily, fontStyle, fontFileUrl });
|
this.$emit("changeFont", { fontFamily, fontStyle, fontFileUrl });
|
||||||
},
|
},
|
||||||
async updateEntries(): Promise<{ entries: SectionsOfMenuListEntries; activeEntry: MenuListEntry }> {
|
async getEntries(): Promise<MenuListEntry[]> {
|
||||||
const choices = this.isStyle ? await this.fonts.getFontStyles(this.fontFamily) : this.fonts.state.fontNames;
|
const x = this.isStyle ? this.fonts.getFontStyles(this.fontFamily) : this.fonts.fontNames();
|
||||||
|
return (await x).map((entry: { name: string; url: URL | undefined }) => ({
|
||||||
|
label: entry.name,
|
||||||
|
value: entry.name,
|
||||||
|
font: entry.url,
|
||||||
|
action: () => this.selectFont(entry.name),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
getActiveEntry(entries: MenuListEntry[]): MenuListEntry {
|
||||||
const selectedChoice = this.isStyle ? this.fontStyle : this.fontFamily;
|
const selectedChoice = this.isStyle ? this.fontStyle : this.fontFamily;
|
||||||
|
|
||||||
let selectedEntry: MenuListEntry | undefined;
|
return entries.find((entry) => entry.value === selectedChoice) as MenuListEntry;
|
||||||
const menuListEntries = choices.map((name) => {
|
|
||||||
const result: MenuListEntry = {
|
|
||||||
label: name,
|
|
||||||
action: async (): Promise<void> => this.selectFont(name),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (name === selectedChoice) selectedEntry = result;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
const entries: SectionsOfMenuListEntries = [menuListEntries];
|
|
||||||
const activeEntry = selectedEntry || { label: "-" };
|
|
||||||
|
|
||||||
return { entries, activeEntry };
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
async fontFamily() {
|
async fontFamily() {
|
||||||
const { entries, activeEntry } = await this.updateEntries();
|
this.entries = await this.getEntries();
|
||||||
this.entries = entries;
|
this.activeEntry = this.getActiveEntry(this.entries);
|
||||||
this.activeEntry = activeEntry;
|
this.highlighted = this.activeEntry;
|
||||||
},
|
},
|
||||||
async fontStyle() {
|
async fontStyle() {
|
||||||
const { entries, activeEntry } = await this.updateEntries();
|
this.entries = await this.getEntries();
|
||||||
this.entries = entries;
|
this.activeEntry = this.getActiveEntry(this.entries);
|
||||||
this.activeEntry = activeEntry;
|
this.highlighted = this.activeEntry;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
LayoutRow,
|
||||||
IconLabel,
|
IconLabel,
|
||||||
MenuList,
|
MenuList,
|
||||||
LayoutRow,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,27 @@
|
||||||
import { reactive, readonly } from "vue";
|
import { reactive } from "vue";
|
||||||
|
|
||||||
import { Editor } from "@/wasm-communication/editor";
|
import { Editor } from "@/wasm-communication/editor";
|
||||||
import { TriggerFontLoad } from "@/wasm-communication/messages";
|
import { TriggerFontLoad } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export function createFontsState(editor: Editor) {
|
export function createFontsState(editor: Editor) {
|
||||||
const state = reactive({
|
const state = reactive({});
|
||||||
fontNames: [] as string[],
|
|
||||||
});
|
|
||||||
|
|
||||||
async function getFontStyles(fontFamily: string): Promise<string[]> {
|
function createURL(font: string): URL {
|
||||||
|
const url = new URL("https://fonts.googleapis.com/css2");
|
||||||
|
url.searchParams.set("display", "swap");
|
||||||
|
url.searchParams.set("family", font);
|
||||||
|
url.searchParams.set("text", font);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fontNames(): Promise<{ name: string; url: URL | undefined }[]> {
|
||||||
|
return (await fontList).map((font) => ({ name: font.family, url: createURL(font.family) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFontStyles(fontFamily: string): Promise<{ name: string; url: URL | undefined }[]> {
|
||||||
const font = (await fontList).find((value) => value.family === fontFamily);
|
const font = (await fontList).find((value) => value.family === fontFamily);
|
||||||
return font?.variants || [];
|
return font?.variants.map((variant) => ({ name: variant, url: undefined })) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFontFileUrl(fontFamily: string, fontStyle: string): Promise<string | undefined> {
|
async function getFontFileUrl(fontFamily: string, fontStyle: string): Promise<string | undefined> {
|
||||||
|
|
@ -58,17 +68,12 @@ export function createFontsState(editor: Editor) {
|
||||||
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 };
|
||||||
});
|
});
|
||||||
state.fontNames = result.map((value) => value.family);
|
|
||||||
|
|
||||||
resolve(result);
|
resolve(result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { state, fontNames, getFontStyles, getFontFileUrl };
|
||||||
state: readonly(state) as typeof state,
|
|
||||||
getFontStyles,
|
|
||||||
getFontFileUrl,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
export type FontsState = ReturnType<typeof createFontsState>;
|
export type FontsState = ReturnType<typeof createFontsState>;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue