mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 19:08:05 +08:00
Font selection for text layers (#585)
* Add font dropdown * Add fonts * Font tool options * Fix tests * Replace http with https * Add variant selection * Do not embed default font * Use proxied font list API * Change default font to Merriweather * Remove outdated comment * Specify font once & load font into foreignobject * Fix tests * Rename variant to font_style * Change TextAreaInput to use FieldInput (WIP, breaks functionality) * Fix textarea functionality * Fix types * Add weight name mapping * Change labeling of "Italic" * Remove commented HTML node * Rename font "name" to "font_family" and "file" "font_file" * Fix errors * Fix fmt Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
parent
916a575446
commit
239aa03453
@@ -158,6 +158,10 @@ img {
|
||||
background-color: var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable-x.scrollable-y {
|
||||
|
||||
@@ -287,10 +287,14 @@ import {
|
||||
TriggerViewportResize,
|
||||
DisplayRemoveEditableTextbox,
|
||||
DisplayEditableTextbox,
|
||||
TriggerFontLoad,
|
||||
TriggerDefaultFontLoad,
|
||||
} from "@/dispatcher/js-messages";
|
||||
|
||||
import { textInputCleanup } from "@/lifetime/input";
|
||||
|
||||
import { loadDefaultFont, setLoadDefaultFontCallback } from "@/utilities/fonts";
|
||||
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
@@ -453,6 +457,14 @@ export default defineComponent({
|
||||
this.editor.dispatcher.subscribeJsMessage(TriggerTextCommit, () => {
|
||||
if (this.textInput) this.editor.instance.on_change_text(textInputCleanup(this.textInput.innerText));
|
||||
});
|
||||
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(TriggerDefaultFontLoad, loadDefaultFont);
|
||||
this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, async (triggerTextCopy) => {
|
||||
// Clipboard API supported?
|
||||
if (!navigator.clipboard) return;
|
||||
@@ -514,6 +526,7 @@ export default defineComponent({
|
||||
// TODO(mfish33): Replace with initialization system Issue:#524
|
||||
// Get initial Document Bar
|
||||
this.editor.instance.init_document_bar();
|
||||
setLoadDefaultFontCallback((font: string, data: Uint8Array) => this.editor.instance.on_font_load(font, data, true));
|
||||
},
|
||||
data() {
|
||||
const documentModeEntries: SectionsOfMenuListEntries = [
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
||||
/>
|
||||
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<TextAreaInput v-if="component.kind === 'TextAreaInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<FontInput
|
||||
v-if="component.kind === 'FontInput'"
|
||||
v-bind="component.props"
|
||||
@changeFont="(value: { name: string, style: string, file: string }) => updateLayout(component.widget_id, value)"
|
||||
/>
|
||||
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
||||
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
|
||||
@@ -28,10 +34,24 @@
|
||||
|
||||
<style lang="scss">
|
||||
.widget-row {
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
--widget-height: 24px;
|
||||
min-height: var(--widget-height);
|
||||
line-height: var(--widget-height);
|
||||
margin: calc((24px - var(--widget-height)) / 2 + 4px) 0;
|
||||
|
||||
&.icon-label.size-12 {
|
||||
--widget-height: 12px;
|
||||
}
|
||||
|
||||
&.icon-label.size-16 {
|
||||
--widget-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,9 +63,11 @@ import { WidgetRow } from "@/dispatcher/js-messages";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
|
||||
import FontInput from "@/components/widgets/inputs/FontInput.vue";
|
||||
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
||||
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
|
||||
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
|
||||
import TextAreaInput from "@/components/widgets/inputs/TextAreaInput.vue";
|
||||
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
@@ -73,6 +95,8 @@ export default defineComponent({
|
||||
TextLabel,
|
||||
IconLabel,
|
||||
ColorInput,
|
||||
FontInput,
|
||||
TextAreaInput,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<template>
|
||||
<LayoutRow class="field-input" :class="{ disabled }">
|
||||
<input
|
||||
v-if="!textarea"
|
||||
:class="{ 'has-label': label }"
|
||||
:id="`field-input-${id}`"
|
||||
ref="input"
|
||||
@@ -15,6 +16,22 @@
|
||||
@keydown.enter="() => $emit('textChanged')"
|
||||
@keydown.esc="() => $emit('cancelTextChange')"
|
||||
/>
|
||||
<textarea
|
||||
v-else
|
||||
:class="{ 'has-label': label }"
|
||||
:id="`field-input-${id}`"
|
||||
class="scrollable-y"
|
||||
data-scrollable-y
|
||||
ref="input"
|
||||
v-model="inputValue"
|
||||
:spellcheck="spellcheck"
|
||||
:disabled="disabled"
|
||||
@focus="() => $emit('textFocused')"
|
||||
@blur="() => $emit('textChanged')"
|
||||
@change="() => $emit('textChanged')"
|
||||
@keydown.ctrl.enter="() => $emit('textChanged')"
|
||||
@keydown.esc="() => $emit('cancelTextChange')"
|
||||
></textarea>
|
||||
<label v-if="label" :for="`field-input-${id}`">{{ label }}</label>
|
||||
<slot></slot>
|
||||
</LayoutRow>
|
||||
@@ -23,7 +40,7 @@
|
||||
<style lang="scss">
|
||||
.field-input {
|
||||
min-width: 80px;
|
||||
height: 24px;
|
||||
height: auto;
|
||||
position: relative;
|
||||
border-radius: 2px;
|
||||
background: var(--color-1-nearblack);
|
||||
@@ -43,7 +60,8 @@
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
input {
|
||||
input,
|
||||
textarea {
|
||||
flex: 1 1 100%;
|
||||
width: 0;
|
||||
min-width: 30px;
|
||||
@@ -55,6 +73,9 @@
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
|
||||
&:not(:focus).has-label {
|
||||
@@ -72,11 +93,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: calc(18px * 4);
|
||||
margin: 3px;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-2-mildblack);
|
||||
|
||||
label,
|
||||
input {
|
||||
input,
|
||||
textarea {
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
@@ -95,6 +125,7 @@ export default defineComponent({
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
spellcheck: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
textarea: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
165
frontend/src/components/widgets/inputs/FontInput.vue
Normal file
165
frontend/src/components/widgets/inputs/FontInput.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<LayoutRow class="dropdown-input">
|
||||
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" @click="() => clickDropdownBox()" data-hover-menu-spawner>
|
||||
<span>{{ activeEntry.label }}</span>
|
||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||
</LayoutRow>
|
||||
<MenuList
|
||||
v-model:activeEntry="activeEntry"
|
||||
@widthChanged="(newWidth: number) => onWidthChanged(newWidth)"
|
||||
:menuEntries="menuEntries"
|
||||
:direction="'Bottom'"
|
||||
:scrollableY="true"
|
||||
ref="menuList"
|
||||
/>
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.dropdown-input {
|
||||
position: relative;
|
||||
|
||||
.dropdown-box {
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
background: var(--color-1-nearblack);
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
|
||||
.dropdown-icon {
|
||||
margin: 4px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0;
|
||||
margin-left: 8px;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.dropdown-icon + span {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin: 6px 2px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
span {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-2-mildblack);
|
||||
|
||||
span {
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list .floating-menu-container .floating-menu-content {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { fontNames, getFontFile, getFontStyles } from "@/utilities/fonts";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import MenuList, { MenuListEntry, SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:fontFamily", "update:fontStyle", "changeFont"],
|
||||
props: {
|
||||
fontFamily: { type: String as PropType<string>, required: true },
|
||||
fontStyle: { type: String as PropType<string>, required: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
isStyle: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
const { menuEntries, activeEntry } = this.updateEntries();
|
||||
return {
|
||||
menuEntries,
|
||||
activeEntry,
|
||||
minWidth: 0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
clickDropdownBox() {
|
||||
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);
|
||||
|
||||
{
|
||||
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 });
|
||||
}
|
||||
},
|
||||
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 result: MenuListEntry = {
|
||||
label: name,
|
||||
action: (): void => this.selectFont(name),
|
||||
};
|
||||
return result;
|
||||
}),
|
||||
];
|
||||
|
||||
const activeEntry = selectedIndex < 0 ? { label: "-" } : menuEntries.flat()[selectedIndex];
|
||||
|
||||
return { menuEntries, activeEntry };
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
fontFamily() {
|
||||
const { menuEntries, activeEntry } = this.updateEntries();
|
||||
this.menuEntries = menuEntries;
|
||||
this.activeEntry = activeEntry;
|
||||
},
|
||||
fontStyle() {
|
||||
const { menuEntries, activeEntry } = this.updateEntries();
|
||||
this.menuEntries = menuEntries;
|
||||
this.activeEntry = activeEntry;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconLabel,
|
||||
MenuList,
|
||||
LayoutRow,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
74
frontend/src/components/widgets/inputs/TextAreaInput.vue
Normal file
74
frontend/src/components/widgets/inputs/TextAreaInput.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<FieldInput
|
||||
:textarea="true"
|
||||
class="text-area-input"
|
||||
:class="{ 'has-label': label }"
|
||||
v-model:value="inputValue"
|
||||
:label="label"
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
ref="fieldInput"
|
||||
></FieldInput>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:value", "commitText"],
|
||||
props: {
|
||||
value: { type: String as PropType<string>, required: true },
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
inputValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value: string) {
|
||||
this.$emit("update:value", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTextFocused() {
|
||||
this.editing = true;
|
||||
},
|
||||
// Called only when `value` is changed from the <textarea> element via user input and committed, either
|
||||
// via the `change` event or when the <input> element is defocused (with the `blur` event binding)
|
||||
onTextChanged() {
|
||||
// The `inputElement.blur()` call in `onCancelTextChange()` causes itself to be run again, so this if statement skips a second run
|
||||
if (!this.editing) return;
|
||||
|
||||
this.onCancelTextChange();
|
||||
|
||||
// TODO: Find a less hacky way to do this
|
||||
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLTextAreaElement;
|
||||
this.$emit("commitText", inputElement.value);
|
||||
|
||||
// Required if value is not changed by the parent component upon update:value event
|
||||
inputElement.value = this.value;
|
||||
},
|
||||
onCancelTextChange() {
|
||||
this.editing = false;
|
||||
|
||||
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLTextAreaElement;
|
||||
inputElement.blur();
|
||||
},
|
||||
},
|
||||
components: { FieldInput },
|
||||
});
|
||||
</script>
|
||||
@@ -388,6 +388,8 @@ export class IndexedDbDocumentDetails extends DocumentDetails {
|
||||
id!: string;
|
||||
}
|
||||
|
||||
export class TriggerDefaultFontLoad extends JsMessage {}
|
||||
|
||||
export class TriggerIndexedDbWriteDocument extends JsMessage {
|
||||
document!: string;
|
||||
|
||||
@@ -403,6 +405,10 @@ export class TriggerIndexedDbRemoveDocument extends JsMessage {
|
||||
document_id!: string;
|
||||
}
|
||||
|
||||
export class TriggerFontLoad extends JsMessage {
|
||||
font!: string;
|
||||
}
|
||||
|
||||
export interface WidgetLayout {
|
||||
layout_target: unknown;
|
||||
layout: LayoutRow[];
|
||||
@@ -427,7 +433,19 @@ export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow
|
||||
return Boolean((layoutRow as WidgetSection).layout);
|
||||
}
|
||||
|
||||
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput" | "TextLabel" | "IconLabel" | "ColorInput";
|
||||
export type WidgetKind =
|
||||
| "NumberInput"
|
||||
| "Separator"
|
||||
| "IconButton"
|
||||
| "PopoverButton"
|
||||
| "OptionalInput"
|
||||
| "RadioInput"
|
||||
| "TextInput"
|
||||
| "TextAreaInput"
|
||||
| "TextLabel"
|
||||
| "IconLabel"
|
||||
| "ColorInput"
|
||||
| "FontInput";
|
||||
|
||||
export interface Widget {
|
||||
kind: WidgetKind;
|
||||
@@ -522,9 +540,11 @@ export const messageConstructors: Record<string, MessageMaker> = {
|
||||
DisplayEditableTextbox,
|
||||
UpdateImageData,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerDefaultFontLoad,
|
||||
TriggerFileDownload,
|
||||
TriggerFileUpload,
|
||||
TriggerIndexedDbRemoveDocument,
|
||||
TriggerFontLoad,
|
||||
TriggerIndexedDbWriteDocument,
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy,
|
||||
|
||||
81
frontend/src/utilities/fonts.ts
Normal file
81
frontend/src/utilities/fonts.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
const fontListAPI = "https://api.graphite.rs/font-list";
|
||||
|
||||
// Taken from https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping
|
||||
const weightNameMapping = new Map([
|
||||
[100, "Thin"],
|
||||
[200, "Extra Light"],
|
||||
[300, "Light"],
|
||||
[400, "Normal"],
|
||||
[500, "Medium"],
|
||||
[600, "Semi Bold"],
|
||||
[700, "Bold"],
|
||||
[800, "Extra Bold"],
|
||||
[900, "Black"],
|
||||
[950, "Extra Black"],
|
||||
]);
|
||||
|
||||
type fontCallbackType = (font: string, data: Uint8Array) => void;
|
||||
|
||||
let fontList = [] as { family: string; variants: string[]; files: Map<string, string> }[];
|
||||
let loadDefaultFontCallback = undefined as fontCallbackType | undefined;
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
function formatFontStyleName(fontStyle: string): string {
|
||||
const isItalic = fontStyle.endsWith("italic");
|
||||
const weight = fontStyle === "regular" || fontStyle === "italic" ? 400 : parseInt(fontStyle, 10);
|
||||
let weightName = "";
|
||||
|
||||
let bestWeight = Infinity;
|
||||
weightNameMapping.forEach((nameChecking, weightChecking) => {
|
||||
if (Math.abs(weightChecking - weight) < bestWeight) {
|
||||
bestWeight = Math.abs(weightChecking - weight);
|
||||
weightName = nameChecking;
|
||||
}
|
||||
});
|
||||
|
||||
return `${weightName}${isItalic ? " Italic" : ""} (${weight})`;
|
||||
}
|
||||
|
||||
export function loadDefaultFont(): void {
|
||||
const font = getFontFile("Merriweather", "Normal (400)");
|
||||
|
||||
if (font) {
|
||||
fetch(font)
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((response) => {
|
||||
if (loadDefaultFontCallback) loadDefaultFontCallback(font, new Uint8Array(response));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function setLoadDefaultFontCallback(callback: fontCallbackType): void {
|
||||
loadDefaultFontCallback = callback;
|
||||
loadDefaultFont();
|
||||
}
|
||||
|
||||
export function fontNames(): string[] {
|
||||
return fontList.map((value) => value.family);
|
||||
}
|
||||
|
||||
export function getFontStyles(fontFamily: string): string[] {
|
||||
const font = fontList.find((value) => value.family === fontFamily);
|
||||
return font ? 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://");
|
||||
}
|
||||
Reference in New Issue
Block a user