Add bundled bus fonts and shared font favorites
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { renderUiCopy, unregisterUiCopySubtree } from "./ui-copy-runtime.js";
|
||||
|
||||
const SOURCE_ORDER = Object.freeze(["automatic", "imported", "system"]);
|
||||
const SOURCE_LABELS = Object.freeze({
|
||||
automatic: "自动字体",
|
||||
@@ -33,23 +35,29 @@ export function filterFontItems(items, query = "") {
|
||||
));
|
||||
}
|
||||
|
||||
export function groupFontItems(items) {
|
||||
return SOURCE_ORDER
|
||||
export function groupFontItems(items, favoriteIds = []) {
|
||||
const favorites = new Set(favoriteIds);
|
||||
return [{ source: "favorites", label: "收藏字体", items: items.filter(item => favorites.has(item.id)) }, ...SOURCE_ORDER
|
||||
.map((source) => ({
|
||||
source,
|
||||
label: SOURCE_LABELS[source],
|
||||
items: items.filter((item) => item.source === source),
|
||||
}))
|
||||
.filter((group) => group.items.length);
|
||||
.filter((group) => group.items.length)];
|
||||
}
|
||||
|
||||
export function createFontPicker({ input, listbox, onCommit }) {
|
||||
export function createFontPicker({ input, listbox, onCommit, onFavorite, onOpen }) {
|
||||
let items = [];
|
||||
let selectedId = "default";
|
||||
let visibleItems = [];
|
||||
let activeIndex = -1;
|
||||
let enabled = false;
|
||||
let closeTimer = null;
|
||||
let favoriteIds = [];
|
||||
let queryText = "";
|
||||
let favoriteBusy = false;
|
||||
let dialogOpen = false;
|
||||
let restoringFocus = false;
|
||||
|
||||
function selectedItem() {
|
||||
return items.find((item) => item.id === selectedId) || null;
|
||||
@@ -101,17 +109,31 @@ export function createFontPicker({ input, listbox, onCommit }) {
|
||||
}
|
||||
|
||||
function render(query = "") {
|
||||
visibleItems = filterFontItems(items, query);
|
||||
const groups = groupFontItems(visibleItems);
|
||||
const bounds = input.getBoundingClientRect();
|
||||
listbox.style.width = `${Math.max(bounds.width, Math.min(440, window.innerWidth - bounds.left - 16))}px`;
|
||||
queryText = query;
|
||||
const filtered = filterFontItems(items, query);
|
||||
const groups = groupFontItems(filtered, favoriteIds);
|
||||
visibleItems = groups.flatMap(group => group.items);
|
||||
const nodes = [];
|
||||
let optionIndex = 0;
|
||||
for (const group of groups) {
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "font-option-group";
|
||||
heading.textContent = group.label;
|
||||
if (group.source === "favorites") renderUiCopy("copy.dynamic.font.favorites", heading, "收藏字体");
|
||||
else heading.textContent = group.label;
|
||||
heading.setAttribute("role", "presentation");
|
||||
nodes.push(heading);
|
||||
if (group.source === "favorites" && !group.items.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "font-options-empty";
|
||||
renderUiCopy(query.trim() ? "copy.dynamic.font.no_matches" : "copy.dynamic.font.empty", empty, query.trim() ? "没有匹配的收藏字体" : "暂无收藏字体");
|
||||
nodes.push(empty);
|
||||
}
|
||||
for (const item of group.items) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "font-option-row";
|
||||
row.setAttribute("role", "presentation");
|
||||
const option = document.createElement("div");
|
||||
option.id = optionId(optionIndex);
|
||||
option.className = "font-option";
|
||||
@@ -124,34 +146,96 @@ export function createFontPicker({ input, listbox, onCommit }) {
|
||||
const source = document.createElement("small");
|
||||
source.textContent = item.style;
|
||||
source.setAttribute("aria-hidden", "true");
|
||||
option.append(label, source);
|
||||
const star = document.createElement("button");
|
||||
star.type = "button";
|
||||
star.className = "font-favorite-button";
|
||||
const favorite = favoriteIds.includes(item.id);
|
||||
star.textContent = favorite ? "★" : "☆";
|
||||
star.setAttribute("aria-label", `${favorite ? "取消收藏" : "收藏"}字体“${item.label}”`);
|
||||
star.setAttribute("aria-pressed", String(favorite));
|
||||
star.disabled = favoriteBusy;
|
||||
star.addEventListener("pointerdown", event => event.stopPropagation());
|
||||
star.addEventListener("click", event => {
|
||||
event.stopPropagation();
|
||||
toggleFavorite(item, favorite);
|
||||
});
|
||||
option.append(label);
|
||||
row.append(option, star, source);
|
||||
option.addEventListener("pointerdown", (event) => event.preventDefault());
|
||||
option.addEventListener("click", () => commit(item));
|
||||
nodes.push(option);
|
||||
row.addEventListener("pointerdown", event => { if (event.target !== star) event.preventDefault(); });
|
||||
row.addEventListener("click", () => commit(item));
|
||||
nodes.push(row);
|
||||
optionIndex += 1;
|
||||
}
|
||||
}
|
||||
if (!visibleItems.length) {
|
||||
if (!filtered.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "font-options-empty";
|
||||
empty.textContent = "没有匹配的字体";
|
||||
nodes.push(empty);
|
||||
}
|
||||
unregisterUiCopySubtree(listbox);
|
||||
listbox.replaceChildren(...nodes);
|
||||
activeIndex = visibleItems.findIndex((item) => item.id === selectedId);
|
||||
if (activeIndex < 0 && visibleItems.length) activeIndex = 0;
|
||||
setActive(activeIndex);
|
||||
}
|
||||
|
||||
async function toggleFavorite(item, favorite) {
|
||||
if (favoriteBusy || dialogOpen) return;
|
||||
if (favorite) {
|
||||
dialogOpen = true;
|
||||
const dialog = document.createElement("dialog");
|
||||
dialog.className = "font-favorite-dialog";
|
||||
dialog.setAttribute("aria-label", "确认取消收藏");
|
||||
const message = document.createElement("p");
|
||||
renderUiCopy("copy.dynamic.font.confirm", message, "确认取消收藏字体“{name}/{style}”?", { name: item.family, style: item.style });
|
||||
dialog.append(message);
|
||||
for (const [value, title] of [["cancel", "取消"], ["confirm", "取消收藏"]]) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
renderUiCopy(value === "cancel" ? "copy.dynamic.font.cancel" : "copy.dynamic.font.remove", button, title);
|
||||
button.addEventListener("click", () => dialog.close(value));
|
||||
dialog.append(button);
|
||||
}
|
||||
document.body.append(dialog);
|
||||
const result = new Promise(resolve => dialog.addEventListener("close", () => resolve(dialog.returnValue === "confirm"), { once: true }));
|
||||
dialog.showModal();
|
||||
const confirmed = await result;
|
||||
unregisterUiCopySubtree(dialog);
|
||||
dialog.remove();
|
||||
dialogOpen = false;
|
||||
if (!confirmed) { restoreInputFocus(); return; }
|
||||
}
|
||||
favoriteBusy = true;
|
||||
render(queryText);
|
||||
try {
|
||||
const result = await onFavorite?.(item, !favorite);
|
||||
if (Array.isArray(result)) favoriteIds = [...result];
|
||||
} finally {
|
||||
favoriteBusy = false;
|
||||
render(queryText);
|
||||
restoreInputFocus();
|
||||
}
|
||||
}
|
||||
|
||||
function restoreInputFocus() {
|
||||
restoringFocus = true;
|
||||
input.focus({ preventScroll: true });
|
||||
restoringFocus = false;
|
||||
}
|
||||
|
||||
function open(query = "") {
|
||||
if (!enabled) return;
|
||||
const wasClosed = listbox.hidden;
|
||||
render(query);
|
||||
listbox.hidden = false;
|
||||
input.setAttribute("aria-expanded", "true");
|
||||
if (wasClosed) onOpen?.();
|
||||
}
|
||||
|
||||
input.addEventListener("focus", () => {
|
||||
if (!enabled) return;
|
||||
if (!enabled || restoringFocus) return;
|
||||
input.select();
|
||||
open();
|
||||
});
|
||||
@@ -179,7 +263,15 @@ export function createFontPicker({ input, listbox, onCommit }) {
|
||||
}
|
||||
});
|
||||
input.addEventListener("blur", () => {
|
||||
closeTimer = window.setTimeout(() => close(), 0);
|
||||
closeTimer = window.setTimeout(() => {
|
||||
if (!dialogOpen && !favoriteBusy && !listbox.contains(document.activeElement)) close();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
listbox.addEventListener("focusout", () => {
|
||||
closeTimer = window.setTimeout(() => {
|
||||
if (!dialogOpen && !favoriteBusy && document.activeElement !== input && !listbox.contains(document.activeElement)) close();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
restoreLabel();
|
||||
@@ -187,7 +279,11 @@ export function createFontPicker({ input, listbox, onCommit }) {
|
||||
setItems(nextItems) {
|
||||
items = normalizeFontItems(nextItems);
|
||||
restoreLabel();
|
||||
if (!listbox.hidden) render();
|
||||
if (!listbox.hidden) render(queryText);
|
||||
},
|
||||
setFavorites(ids) {
|
||||
favoriteIds = Array.isArray(ids) ? [...ids] : [];
|
||||
if (!listbox.hidden && !favoriteBusy && !dialogOpen) render(queryText);
|
||||
},
|
||||
setValue(identifier) {
|
||||
selectedId = typeof identifier === "string" && identifier ? identifier : "default";
|
||||
|
||||
Reference in New Issue
Block a user