feat(字体处理): 统一字体名称,优化粗斜体选择
build.yaml / build (push) Successful in 54s

This commit is contained in:
xiaoqidun committed 2026-10-28 21:33:26 +08:00
1 parent 12aaada982
commit 7a23dcc6dd
10 files changed
+574 -77

No files matched your search

+218 -58
View File
@@ -699,7 +699,7 @@ const fontManager = new FontManager({
onChange: scheduleFontSync,
onPermissionChange: updateFontPermissionHint,
});
const fontPicker = new FontPicker(el.textFont, el.textFontToggle, el.textFontList, changeTextFont, loadEditorFonts);
const fontPicker = new FontPicker(el.textFont, el.textFontToggle, el.textFontList, changeTextFont, loadEditorFonts, groupTextFonts);
const textFormatButtons = [...document.querySelectorAll("[data-text-format]")];
const textBackground = document.querySelector("#textBackground");
const editorInputs = new Map();
@@ -4152,15 +4152,61 @@ function currentTextStyle() {
const text = selectedText(canvasEditor.selected) || state.textDefaults;
if (canvasEditor.input?.rich) {
const rich = canvasEditor.input.rich, layout = richTextLayout(rich.model, ...rich.selection());
return { ...text, ...rich.state(), ...layout, paragraphHeight: layout.lineHeight };
return { ...text, ...rich.state(), ...textFaceStyle(rich.selectedRuns().map(run => run.format)), ...layout, paragraphHeight: layout.lineHeight };
}
if (text.richText) {
const formats = text.richText.runs.map(run => run.format);
const formats = text.richText.runs.map(run => effectiveTextFormat(run.format));
const layout = richTextLayout(text.richText);
return { ...text, ...layout, paragraphHeight: layout.lineHeight, ...Object.fromEntries(["font", "size", "color", "bold", "italic", "underline", "strikeout", "shadow", "background"]
.map(key => [key, formats.every(format => format[key] === formats[0]?.[key]) ? formats[0]?.[key] : undefined])) };
}
return canvasEditor.input?.style ? { ...text, ...canvasEditor.input.style } : text;
if (text.items) return { ...text, ...textFaceStyle(text.items.flatMap(item => textFontRuns(item).map(run => run.format))) };
return text === state.textDefaults ? text : effectiveTextFormat(canvasEditor.input?.style ? { ...text, ...canvasEditor.input.style } : text);
}
function documentFontChoice(id) {
const font = state.doc?.fonts?.find(font => font.id === id);
return { ...(font?.status !== "fallback" && font?.status !== "missing" ? font?.matchedFace : null), id: `embedded:${id}`, name: font?.fontName || id, embedded: true };
}
function textFontChoice(id) {
const rich = canvasEditor.input?.rich;
const choice = rich?.choices.get(id);
return choice ? { ...choice, richKey: id } : id ? documentFontChoice(id) : state.textDefaults.fontChoice;
}
function effectiveTextFormat(format) {
const font = textFontChoice(format.font), face = canvasEditor.input?.rich?.faces.get(format.font);
return { ...format, bold: Boolean(format.bold || font?.bold || Number(face?.weight) >= 600), italic: Boolean(format.italic || font?.italic || face?.style === "italic") };
}
function textFaceStyle(formats) {
formats = formats.map(effectiveTextFormat);
return Object.fromEntries(["bold", "italic"].map(key => [key, formats.every(format => format[key] === formats[0]?.[key]) ? formats[0]?.[key] : undefined]));
}
function textFontRuns(item) {
return (item?.items || [item || state.textDefaults]).flatMap(item => item.richText?.runs || [{ text: item.text || "", format: { font: item.font || "", bold: Boolean(item.bold), italic: Boolean(item.italic) } }]);
}
function textFontSource(font) {
return font.embedded ? "embedded" : font.file || ["stored", "upload"].includes(font.source) ? "upload" : "system";
}
function textFontIdentity(font) {
return `${textFontSource(font)}:${font.embedded ? font.richKey || font.id?.slice(9) : font.id ?? font.postscriptName}`;
}
async function groupTextFonts(fonts, tab = documentTab()) {
if (!fonts.some(font => font.familyKey)) return fonts.map((_, index) => [index]);
const { groups } = await callTabWASM(tab, "ofdgoFontMenu", JSON.stringify({ faces: fonts.map(font => ({ familyKey: font.familyKey, bold: font.bold, italic: font.italic })), sources: fonts.map(textFontSource) }));
return groups;
}
function sharedTextFont(formats) {
const choices = formats.map(format => textFontChoice(format.font));
const first = choices[0];
return first && choices.every(font => font && (font.familyKey && font.familyKey === first.familyKey && textFontSource(font) === textFontSource(first) || textFontIdentity(font) === textFontIdentity(first))) ? formats[0].font : undefined;
}
async function loadRichText(item, firstFace, tab = documentTab(), firstData = null, reflow = false) {
@@ -4260,12 +4306,13 @@ function flushEditorInput(control) {
function flushEditorInputs(event) {
const tasks = [...editorInputs.values()].filter(control => !event || !control.input.contains(event.target))
.map(flushEditorInput).concat(fontPicker.prepareCommit(event)).filter(Boolean);
.map(flushEditorInput).concat(fontPicker.prepareCommit(event), canvasEditor.input?.fontTask).filter(Boolean);
return tasks.length ? Promise.all(tasks) : null;
}
async function changeRangeFormat(key, value) {
if (!state.ready || document.body.hasAttribute("data-busy") || canvasEditor.input?.composing) return;
if (key === "bold" || key === "italic") return changeTextFace(null, key, value ?? currentTextStyle()[key] !== true);
const tab = documentTab(), editing = canvasEditor.input;
try {
if (!canvasEditor.selected && !canvasEditor.input) {
@@ -4414,7 +4461,7 @@ function editorFonts(item) {
for (const id of member.richText ? member.richText.runs.map(run => run.format.font) : [member.font]) {
if (!id || embedded.has(id)) continue;
const font = state.doc?.fonts?.find(font => font.id === id);
embedded.set(id, { id: `embedded:${id}`, name: font?.fontName || (id === member.font ? member.fontName : "") || id,
embedded.set(id, { ...documentFontChoice(id), name: font?.fontName || (id === member.font ? member.fontName : "") || id,
embedded: true, disabled: !canEditObject(member, "textContent") });
}
}
@@ -4430,6 +4477,7 @@ async function loadEditorFonts(requestPermission = true, tab = documentTab(), wa
try {
await fontManager.queryLocal();
refreshEditorFonts();
await loadEditorFonts(false, tab);
} catch (err) {
if (requestPermission && isForegroundDocument(tab)) setStatus(err?.name === "NotAllowedError" ? "系统字体未授权" : String(err.message || err));
} finally {
@@ -4438,7 +4486,7 @@ async function loadEditorFonts(requestPermission = true, tab = documentTab(), wa
})();
tasks.push(target.fontCatalogLoading);
}
if (!target.fontFacesLoading && fontManager.userFonts.some(font => font.enabled && !font.faces)) {
if (!target.fontFacesLoading && (fontManager.userFonts.some(font => font.enabled && !font.faces) || fontManager.catalog.some(font => font.blob && !font.menuFamily && !font.metadataFailed))) {
const wasmSeq = target.wasmSeq;
const current = () => !tab.closed && !target.wasmExited && target.wasmSeq === wasmSeq;
target.fontFacesLoading = (async () => {
@@ -4496,7 +4544,7 @@ function updateTextFonts(item, refresh = false, preserve = false) {
item = selectedText(item);
const rich = canvasEditor.input?.rich;
if (rich) {
const format = rich.state(), id = format.font;
const id = sharedTextFont(rich.selectedRuns().map(run => run.format));
const key = `rich:${id ?? "mixed"}:${rich.faces.size}`;
if (refresh || state.textFontID !== key) {
if (!preserve && document.activeElement === el.textFont && fontPicker.open) return;
@@ -4504,7 +4552,8 @@ function updateTextFonts(item, refresh = false, preserve = false) {
state.textFonts = editorFonts(null);
for (const font of rich.faces.keys()) {
const choice = rich.choices?.get(font);
state.textFonts.unshift({ ...(choice || { id: `embedded:${font}`,
if (choice) state.textFonts = state.textFonts.filter(value => textFontIdentity(value) !== textFontIdentity(choice));
state.textFonts.unshift({ ...(choice || { ...documentFontChoice(font),
name: state.doc?.fonts?.find(value => value.id === font)?.fontName || (font === rich.editing.item.font ? rich.editing.item.fontName : "") || font,
embedded: !rich.imports.has(font) }), richKey: font });
}
@@ -4513,7 +4562,7 @@ function updateTextFonts(item, refresh = false, preserve = false) {
}
return;
}
const id = item?.type === "TextObject" && !item.draft ? item.richText ? currentTextStyle().font : item.font : null;
const id = item?.type === "TextObject" && !item.draft ? sharedTextFont(textFontRuns(item).map(run => run.format)) : null;
const key = item?.items ? `selection:${item.items.map(member => member.id).join(",")}:${id || ""}`
: item?.richText ? `rich:${item.id}:${item.richText.runs.map(run => run.format.font).join(",")}` : id;
if (refresh || state.textFontID !== key) {
@@ -4531,59 +4580,170 @@ function updateTextFonts(item, refresh = false, preserve = false) {
}
}
async function changeTextFont() {
function textFontStyleKey(format) {
return JSON.stringify([format.font || "", Boolean(format.bold), Boolean(format.italic)]);
}
async function prepareTextFaces(tab, runs, font, key, value, rich) {
const { openSeq, wasmSeq, doc } = tab.state;
const current = () => !tab.closed && openSeq === tab.state.openSeq && wasmSeq === tab.state.wasmSeq && doc === tab.state.doc && (!rich || canvasEditor.input === rich.editing);
const callWASM = (name, ...args) => {
if (!current()) throw new Error("文档已更改");
return callTabWASM(tab, name, ...args);
};
const choices = new Map(), data = new Map();
const originals = new Map(runs.map(run => [run.format.font || "", textFontChoice(run.format.font)]));
const add = font => { if (font && !font.disabled && !choices.has(textFontIdentity(font))) choices.set(textFontIdentity(font), { ...font }); };
if (font) add(font);
if (rich) for (const id of rich.faces.keys()) add({ ...textFontChoice(id), richKey: id });
for (const original of originals.values()) add(original);
for (const font of tab.state.doc?.fonts || []) if (font.embedded || font.status === "matched") add(documentFontChoice(font.id));
for (const font of fontManager.editorFonts()) add(font);
const read = async choice => {
const id = textFontIdentity(choice);
if (!data.has(id)) data.set(id, choice.richKey && (rich?.imports.get(choice.richKey) || rich?.resources.get(choice.richKey)) || await readTextFont(choice, tab));
if (!current()) throw new Error("文档已更改");
return data.get(id);
};
const candidates = [...choices.values()];
for (const choice of candidates) {
if (choice.menuFamily || ![...originals.values()].some(font => textFontIdentity(font || {}) === textFontIdentity(choice)) && (!font || textFontIdentity(choice) !== textFontIdentity(font))) continue;
const [metadata] = await callWASM("ofdgoFontFaces", await read(choice));
if (metadata) Object.assign(choice, metadata, { index: choice.index });
}
const groups = await groupTextFonts(candidates, tab), changes = new Map(), prepared = new Map();
for (const run of runs) {
const from = { font: run.format.font || "", bold: Boolean(run.format.bold), italic: Boolean(run.format.italic) };
const original = choices.get(textFontIdentity(originals.get(from.font) || {}));
const target = font ? choices.get(textFontIdentity(font)) : original;
if (!target) throw new Error("字体不可用");
const bold = key === "bold" ? value : Boolean(from.bold || original?.bold);
const italic = key === "italic" ? value : Boolean(from.italic || original?.italic);
const group = groups.find(group => group.includes(candidates.indexOf(target))).map(index => candidates[index]);
let { index } = await callWASM("ofdgoFontMenu", JSON.stringify({ faces: group.map(({ bold, italic }) => ({ bold, italic })), bold, italic }));
if (index < 0 && font) index = group.indexOf(target);
if (index < 0) throw new Error("此字体没有对应款式");
const selected = group[index], identity = textFontIdentity(selected);
if (!prepared.has(identity)) prepared.set(identity, { choice: selected, key: selected.richKey || (selected.embedded ? selected.id.slice(9) : `temp-font-${crypto.randomUUID()}`), data: await read(selected), text: "" });
const entry = prepared.get(identity);
entry.text += run.text;
changes.set(textFontStyleKey(from), { from, to: { font: entry.key,
bold: !key && selected.bold && bold ? from.bold : Boolean(bold || selected.bold),
italic: !key && selected.italic && italic ? from.italic : Boolean(italic || selected.italic) } });
}
for (const entry of prepared.values()) {
try { await callWASM("ofdgoCheckTextFont", entry.data, entry.text); }
catch (error) {
if (!error.missingGlyphs || !entry.choice.embedded || !entry.choice.familyKey) throw error;
let recovered = false;
for (const choice of candidates.filter(choice => !choice.embedded && choice.familyKey === entry.choice.familyKey && choice.postscriptName === entry.choice.postscriptName)) {
try {
const bytes = await read(choice);
await callWASM("ofdgoCheckTextFont", bytes, entry.text);
entry.data = bytes;
entry.choice = { ...choice, richKey: entry.key };
recovered = true;
break;
} catch { if (!current()) throw error; }
}
if (!recovered) throw error;
}
if (!current()) throw new Error("文档已更改");
if (rich) entry.face = await new FontFace(`ofdgo-edit-font-${crypto.randomUUID()}`, entry.data, fontFaceStyle(entry.data)).load();
}
return { changes: [...changes.values()], prepared: [...prepared.values()], fonts: [...prepared.values()].filter(entry => !entry.choice.embedded).map(({ key, data, text }) => ({ key, data, text })) };
}
function changeTextFont() {
const font = state.textFonts[Number(fontPicker.value)];
if (font && !font.disabled) return changeTextFace(font);
}
async function changeTextFace(font, key, value) {
const tab = documentTab(), state = tab.state;
const openSeq = state.openSeq;
const callWASM = (name, ...args) => callTabWASM(tab, name, ...args);
const item = selectedText(canvasEditor.selected);
const font = state.textFonts[Number(fontPicker.value)];
if (!state.ready || document.body.hasAttribute("data-busy") || !font || font.disabled) return;
if (!state.ready || document.body.hasAttribute("data-busy")) return;
const editing = canvasEditor.input;
if (editing) {
if (editing.composing) return;
try {
if (!await ensureRichInput() || canvasEditor.input !== editing || !isForegroundDocument(tab)) return;
} catch (error) {
if (canvasEditor.input === editing && isForegroundDocument(tab)) showError(error, false);
return;
}
editing.rich?.remember();
tab.editTask = true;
setBusy(true, "", 0, "", tab);
try {
const data = font.richKey && editing.rich?.imports.has(font.richKey) ? editing.rich.imports.get(font.richKey) : await readTextFont(font, tab);
if (!isForegroundDocument(tab) || openSeq !== state.openSeq || canvasEditor.input !== editing) return;
const [start, end] = editing.rich?.selection() || [0, editTextValue(editing.input).length];
await callWASM("ofdgoCheckTextFont", data, editTextValue(editing.input).slice(start, end));
if (!isForegroundDocument(tab) || openSeq !== state.openSeq || canvasEditor.input !== editing) return;
const face = new FontFace(`ofdgo-edit-font-${crypto.randomUUID()}`, data, fontFaceStyle(data));
await face.load();
if (!isForegroundDocument(tab) || openSeq !== state.openSeq || canvasEditor.input !== editing) return;
editing.fontChoice = font;
if (item.draft && !font.richKey) state.textDefaults.fontChoice = font;
if (editing.rich) {
const key = font.richKey || (font.embedded ? font.id.slice(9) : `temp-font-${crypto.randomUUID()}`);
editing.rich.choices ||= new Map();
editing.rich.choices.set(key, font);
canvasEditor.setTextFont(face, font.embedded ? null : data, key);
} else canvasEditor.setTextFont(face, data);
} catch (err) {
if (isForegroundDocument(tab) && openSeq === state.openSeq) {
updateTextFonts(canvasEditor.selected, true);
showError(err, false);
const current = () => isForegroundDocument(tab) && openSeq === state.openSeq && canvasEditor.input === editing;
if (!editing && !item) {
const source = font || state.textDefaults.fontChoice;
const bold = key === "bold" ? value : Boolean(state.textDefaults.bold), italic = key === "italic" ? value : Boolean(state.textDefaults.italic);
let selected = source;
if (source?.familyKey && (key || bold || italic)) {
const busy = {};
tab.editTask = busy;
setBusy(true, "", 0, "", tab);
try {
const fonts = fontManager.editorFonts().filter(font => font.familyKey === source.familyKey && textFontSource(font) === textFontSource(source)), groups = await groupTextFonts(fonts, tab);
if (!current()) return;
const index = fonts.findIndex(choice => textFontIdentity(choice) === textFontIdentity(source));
const faces = (groups.find(group => group.includes(index)) || []).map(index => fonts[index]);
const result = await callTabWASM(tab, "ofdgoFontMenu", JSON.stringify({ faces: faces.map(({ bold, italic }) => ({ bold, italic })), bold, italic }));
if (!current()) return;
selected = faces[result.index] || (font ? source : null);
if (!selected) throw new Error("此字体没有对应款式");
} catch (error) {
if (current()) showError(error, false);
return;
} finally {
if (tab.editTask === busy) { tab.editTask = false; setBusy(false, "", 0, "", tab); }
}
} finally {
tab.editTask = false;
setBusy(false, "", 0, "", tab);
}
} else if (item?.type === "TextObject") {
if (canEditObject(item, "replaceFont") && (!font.embedded || currentTextStyle().font !== font.id.slice(9))) {
const changed = await changeDocument("ofdgoStyleText", { ...item, id: (item.items || [item]).map(member => member.id) }, () => readTextFont(font, tab), 0, null);
if (isForegroundDocument(tab) && (changed || openSeq === state.openSeq)) updateTextFonts(canvasEditor.selected, true);
}
} else {
state.textDefaults.fontChoice = font;
state.textDefaults.fontChoice = selected;
state.textDefaults.bold = Boolean(bold || selected?.bold);
state.textDefaults.italic = Boolean(italic || selected?.italic);
updateTextFonts(null, true);
updateObjectControls(null);
return;
}
if (!editing && item?.type === "TextObject") {
if (!key && font?.embedded && textFontRuns(item).every(run => run.format.font === font.id.slice(9))) return;
const reflow = Boolean(key && !canEditObject(item, "layoutKnown"));
if (!canEditObject(item, "replaceFont") || reflow && !confirmTextReflow(item)) return;
await changeDocument("ofdgoStyleTextFonts", { ...item, id: (item.items || [item]).map(member => member.id) }, async () => {
const result = await prepareTextFaces(tab, textFontRuns(item), font, key, value);
return { changes: JSON.stringify(result.changes), fonts: result.fonts, reflow };
});
if (isForegroundDocument(tab)) updateTextFonts(canvasEditor.selected, true);
return;
}
if (editing.composing) return;
const task = (async () => {
try {
if (!await ensureRichInput() || !current()) return;
const rich = editing.rich;
rich.remember();
const runs = rich.selectedRuns(), before = rich.model.runs, caret = rich.selection().join();
const busy = {};
tab.editTask = busy;
setBusy(true, "", 0, "", tab);
try {
const result = await prepareTextFaces(tab, runs, font, key, value, rich);
if (!current() || rich.model.runs !== before || rich.selection().join() !== caret || editing.composing || editing.saving) return;
for (const entry of result.prepared) {
const previous = rich.faces.get(entry.key);
rich.faces.set(entry.key, entry.face);
if (previous && ![...rich.faces.values()].includes(previous)) document.fonts.delete(previous);
if (!entry.choice.embedded) rich.imports.set(entry.key, entry.data);
rich.choices.set(entry.key, entry.choice);
document.fonts.add(entry.face);
}
const mapping = new Map(result.changes.map(change => [textFontStyleKey(change.from), change.to]));
rich.apply(format => ({ ...format, ...mapping.get(textFontStyleKey(format)) }));
if (item?.draft && font && !font.richKey) state.textDefaults.fontChoice = font;
updateTextFonts(canvasEditor.selected, true);
updateObjectControls(canvasEditor.selected);
} finally {
if (tab.editTask === busy) { tab.editTask = false; setBusy(false, "", 0, "", tab); }
}
} catch (error) {
if (current()) { updateTextFonts(canvasEditor.selected, true); showError(error, false); }
}
})();
if (editing) editing.fontTask = task;
try { await task; }
finally { if (editing?.fontTask === task) editing.fontTask = null; }
}
async function insertObject(event) {
@@ -4916,8 +5076,8 @@ async function changeDocument(name, item, ...args) {
const scopedRichText = scoped && name === "ofdgoSaveRichText";
if (scopedRichText) args.push(scope);
const textSelection = name === "ofdgoUpdateTextSelection";
if (scoped && !operation && !textSelection && !scopedRichText && name !== "ofdgoLayoutTexts") throw new Error("内部对象暂不支持此操作");
const doc = ["ofdgoStyleText", "ofdgoFormatText", "ofdgoLayoutTexts"].includes(name)
if (scoped && !operation && !textSelection && !scopedRichText && !["ofdgoLayoutTexts", "ofdgoStyleTextFonts"].includes(name)) throw new Error("内部对象暂不支持此操作");
const doc = ["ofdgoStyleText", "ofdgoFormatText", "ofdgoLayoutTexts", "ofdgoStyleTextFonts"].includes(name)
? await callWASM(name, item.index, ids, ...args)
: textSelection
? await callWASM(name, item.index, item.selectionIDs, ...args)
+7 -2
View File
@@ -236,6 +236,10 @@ export class RichTextSession {
const runs = sliceRuns(this.model.runs, start, end);
return Object.fromEntries(FORMAT_KEYS.map(key => [key, runs.every(run => run.format[key] === runs[0]?.format[key]) ? runs[0]?.format[key] : undefined]));
}
selectedRuns() {
const [start, end] = this.selection();
return start === end ? [{ text: "", format: { ...this.typing } }] : sliceRuns(this.model.runs, start, end);
}
remember() {
this.selection();
this.entries[this.index].caret = [...this.caret];
@@ -302,13 +306,14 @@ export class RichTextSession {
this.remember();
const [start, end] = this.selection();
const before = this.model.runs;
const apply = format => typeof values === "function" ? values(format) : { ...format, ...values };
this.caret = this.caret[0] > this.caret[1] ? [end, start] : [start, end];
if (start !== end) this.model.runs = normalizeRuns([
...sliceRuns(this.model.runs, 0, start),
...sliceRuns(this.model.runs, start, end).map(run => ({ ...run, format: { ...run.format, ...values } })),
...sliceRuns(this.model.runs, start, end).map(run => ({ ...run, format: apply(run.format) })),
...sliceRuns(this.model.runs, end, this.text.length),
]);
this.typing = { ...this.typing, ...values };
this.typing = apply(this.typing);
this.record(before);
this.render();
this.editor.options.onTextChange();
+46 -11
View File
@@ -1,8 +1,8 @@
const DATABASE = "ofdgo";
export class FontPicker {
constructor(input, toggle, list, onChange, onOpen) {
Object.assign(this, { input, toggle, list, onChange, onOpen, fonts: [], value: "", active: -1, open: false });
constructor(input, toggle, list, onChange, onOpen, groupFonts) {
Object.assign(this, { input, toggle, list, onChange, onOpen, groupFonts, fonts: [], groups: [], value: "", active: -1, open: false });
input.addEventListener("input", () => this.show(input.value));
input.addEventListener("focus", () => input.select());
input.addEventListener("pointerdown", event => { if (event.button === 0 && !this.open) event.preventDefault(); });
@@ -28,9 +28,10 @@ export class FontPicker {
const open = preserve && this.open;
if (!open) this.close();
this.fonts = fonts;
this.groups = fonts.map((_, index) => [index]);
this.names = new Map();
for (const font of fonts) {
const name = (font.fullName || font.name).normalize("NFKC").toLocaleLowerCase();
const name = (font.menuFamily || font.fullName || font.name).normalize("NFKC").toLocaleLowerCase();
this.names.set(name, (this.names.get(name) || 0) + 1);
}
const key = font => font?.richKey || font?.id || font?.postscriptName;
@@ -41,6 +42,22 @@ export class FontPicker {
const index = active ? this.matches.findIndex(({ font }) => !font.disabled && key(font) === key(active)) : -1;
if (index >= 0) this.activate(index);
} else this.restore();
if (this.groupFonts) this.grouping = this.groupFonts(fonts).then(groups => {
if (this.fonts !== fonts) return;
this.groups = groups;
this.names.clear();
for (const group of groups) {
const font = fonts[group[0]], name = (font.menuFamily || font.fullName || font.name).normalize("NFKC").toLocaleLowerCase();
this.names.set(name, (this.names.get(name) || 0) + 1);
}
if (this.open) {
const active = this.matches[this.active]?.index;
this.show(this.query, false);
const index = this.matches.findIndex(({ group, font }) => group.includes(active) && !font.disabled);
if (index >= 0) this.activate(index);
}
else this.restore();
}).catch(() => {});
}
setDisabled(disabled) {
@@ -50,7 +67,7 @@ export class FontPicker {
restore() {
const font = this.fonts[Number(this.value)];
const value = font?.fullName || font?.name || "";
const value = font?.menuFamily || font?.fullName || font?.name || "";
if (this.input.value !== value) {
this.input.value = value;
if (document.activeElement === this.input) this.input.select();
@@ -60,7 +77,7 @@ export class FontPicker {
}
sourceLabel(font) {
const name = (font.fullName || font.name).normalize("NFKC").toLocaleLowerCase();
const name = (font.menuFamily || font.fullName || font.name).normalize("NFKC").toLocaleLowerCase();
if (this.names.get(name) < 2) return "";
return font.embedded ? "内嵌" : font.file || ["stored", "upload"].includes(font.source) ? "上传" : "系统";
}
@@ -70,8 +87,14 @@ export class FontPicker {
this.query = query;
const normalize = value => value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/g, " ").trim();
const terms = normalize(query).split(" ").filter(Boolean);
this.matches = this.fonts.map((font, index) => ({ font, index })).filter(({ font }) => {
const name = normalize([font.fullName, font.name, font.family, font.style, font.postscriptName, ...(font.names || [])].filter(Boolean).join(" "));
this.matches = this.groups.map(group => {
const index = group.find(index => !this.fonts[index].bold && !this.fonts[index].italic && !this.fonts[index].disabled) ?? group[0];
return { font: this.fonts[index], index, group };
}).filter(({ group }) => {
const name = normalize(group.flatMap(index => {
const font = this.fonts[index];
return [font.menuFamily, font.menuStyle, font.fullName, font.name, font.family, font.style, font.postscriptName, ...(font.names || [])];
}).filter(Boolean).join(" "));
return terms.every(term => name.includes(term));
});
this.list.replaceChildren();
@@ -82,7 +105,7 @@ export class FontPicker {
option.dataset.index = String(index);
option.setAttribute("data-disabled", String(Boolean(font.disabled)));
const name = document.createElement("span");
name.textContent = font.fullName || font.name;
name.textContent = font.menuFamily || font.fullName || font.name;
option.append(name);
const source = this.sourceLabel(font);
if (source) {
@@ -103,7 +126,7 @@ export class FontPicker {
if (!this.open) this.list.showPopover();
this.open = true;
this.position();
const selected = this.matches.findIndex(({ font, index }) => !font.disabled && String(index) === this.value);
const selected = this.matches.findIndex(({ font, group }) => !font.disabled && group.includes(Number(this.value)));
this.activate(selected < 0 ? this.matches.findIndex(({ font }) => !font.disabled) : selected);
if (load) this.onOpen?.();
}
@@ -140,7 +163,7 @@ export class FontPicker {
commitExact() {
const value = this.input.value.trim().toLocaleLowerCase();
const matches = font => font && !font.disabled
&& [font.fullName, font.name, font.postscriptName, ...(font.names || [])].some(name => name?.toLocaleLowerCase() === value);
&& [font.menuFamily, font.fullName, font.name, font.postscriptName, ...(font.names || [])].some(name => name?.toLocaleLowerCase() === value);
const selected = Number(this.value);
const index = matches(this.fonts[selected]) ? selected : this.fonts.findIndex(matches);
if (index >= 0) return this.choose(index);
@@ -340,10 +363,22 @@ export class FontManager {
async loadFaces(inspect, current = () => true) {
while (current()) {
const file = this.userFonts.find(font => font.enabled && !font.faces);
if (!file) return;
if (!file) break;
try { file.faces = await inspect(await this.read(file)); }
catch { if (!current()) return; file.faces = []; }
}
const pending = this.catalog.filter(font => font.blob && !font.menuFamily && !font.metadataFailed);
await Promise.all(Array.from({ length: Math.min(4, pending.length) }, async () => {
while (current() && pending.length) {
const font = pending.shift();
try {
const faces = await inspect(await this.read(font));
const face = faces.find(face => face.postscriptName === font.postscriptName);
if (face) Object.assign(font, { index: face.index, menuFamily: face.menuFamily, menuStyle: face.menuStyle, familyKey: face.familyKey, bold: face.bold, italic: face.italic, names: face.names });
else font.metadataFailed = true;
} catch { if (current()) font.metadataFailed = true; }
}
}));
}
async defaultFont(order, inspect, current = () => true) {