mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
58564b0647
commit
6428a885fa
12 files changed
+462
-72
No files matched your search
@@ -100,7 +100,8 @@
|
||||
<span>插入</span>
|
||||
</div>
|
||||
<div class="editor-group">
|
||||
<div class="text-font"><select id="textFont" aria-label="字体" title="字体" disabled></select><button id="textFontAdd" class="icon-button" type="button" title="添加字体" aria-label="添加字体">+</button></div>
|
||||
<div class="text-font"><div class="font-picker"><input id="textFont" type="text" role="combobox" aria-label="字体" title="字体" aria-autocomplete="list" aria-controls="textFontList" aria-expanded="false" autocomplete="off" spellcheck="false" disabled><button id="textFontToggle" type="button" title="选择字体" aria-label="选择字体" tabindex="-1" disabled></button></div><button id="textFontAdd" class="icon-button" type="button" title="添加字体" aria-label="添加字体">+</button></div>
|
||||
<div id="textFontList" class="font-listbox" role="listbox" aria-label="字体列表" popover="manual"></div>
|
||||
<div class="editor-number"><input id="textSize" type="number" min="1" step="any" required aria-label="字号" disabled><span>pt</span></div>
|
||||
<input id="textColor" class="color-input" type="color" value="#000000" aria-label="文字颜色" title="文字颜色" disabled>
|
||||
<span>文字</span>
|
||||
|
||||
@@ -208,6 +208,78 @@ a:focus-visible {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.font-picker {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
height: var(--control-height);
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.font-picker #textFont {
|
||||
padding: 0 6px;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: #1f2428;
|
||||
}
|
||||
|
||||
.font-picker:focus-within {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
#textFontToggle {
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#textFontToggle::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-right: 1px solid currentColor;
|
||||
border-bottom: 1px solid currentColor;
|
||||
transform: translateY(-2px) rotate(45deg);
|
||||
}
|
||||
|
||||
.font-listbox {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
color: #1f2428;
|
||||
box-shadow: 0 5px 18px rgba(20, 40, 50, 0.14);
|
||||
overflow-y: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.font-listbox [role="option"] {
|
||||
padding: 6px 8px;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.font-listbox [role="option"]:hover,
|
||||
.font-listbox [aria-selected="true"] {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.font-listbox [aria-disabled="true"],
|
||||
.font-search-empty {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.font-search-empty {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
#textColor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
+15
-25
@@ -1,5 +1,5 @@
|
||||
import { CanvasEditor, canEditObject, objectEditReason } from "./ofdgo_edit.js";
|
||||
import { FontManager } from "./ofdgo_font.js";
|
||||
import { FontManager, FontPicker } from "./ofdgo_font.js";
|
||||
|
||||
const MM_TO_PX = 96 / 25.4;
|
||||
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
|
||||
@@ -109,6 +109,8 @@ const el = {
|
||||
textLineHeight: document.querySelector("#textLineHeight"),
|
||||
textSpacing: document.querySelector("#textSpacing"),
|
||||
textFont: document.querySelector("#textFont"),
|
||||
textFontToggle: document.querySelector("#textFontToggle"),
|
||||
textFontList: document.querySelector("#textFontList"),
|
||||
textFontAdd: document.querySelector("#textFontAdd"),
|
||||
textSize: document.querySelector("#textSize"),
|
||||
textColor: document.querySelector("#textColor"),
|
||||
@@ -244,9 +246,11 @@ const fontManager = new FontManager({
|
||||
onChange: scheduleFontSync,
|
||||
onPermissionChange: updateFontPermissionHint,
|
||||
});
|
||||
const fontPicker = new FontPicker(el.textFont, el.textFontToggle, el.textFontList, changeTextFont);
|
||||
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
||||
|
||||
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
fontControls: [el.textFont, el.textFontAdd],
|
||||
fontControls: [el.textFont, el.textFontToggle, el.textFontAdd],
|
||||
busy: () => document.body.hasAttribute("aria-busy"),
|
||||
rotation: () => state.rotation,
|
||||
canInsert: (index) => pageCan("insert", index),
|
||||
@@ -341,7 +345,6 @@ editorClick(el.textWrap, () => changeParagraph(!currentTextStyle().wrap));
|
||||
el.textAlign.addEventListener("change", () => changeParagraph());
|
||||
el.textLineHeight.addEventListener("change", () => changeParagraph());
|
||||
el.textSpacing.addEventListener("change", () => changeParagraph());
|
||||
el.textFont.addEventListener("change", changeTextFont);
|
||||
el.shapeWidth.addEventListener("focus", () => { el.shapeWidth.defaultValue = el.shapeWidth.value; });
|
||||
for (const input of [el.textSize, el.shapeWidth, el.textLineHeight, el.textSpacing]) {
|
||||
input.addEventListener("keydown", (event) => {
|
||||
@@ -942,7 +945,7 @@ async function toggleTextTool() {
|
||||
}
|
||||
if (openSeq !== state.openSeq) return;
|
||||
const style = canEditObject(canvasEditor.selected, "layoutKnown") ? currentTextStyle() : state.textDefaults;
|
||||
const font = state.textFonts[Number(el.textFont.value)];
|
||||
const font = state.textFonts[Number(fontPicker.value)];
|
||||
state.textDefaults = { type: "TextObject", size: style.size, color: style.color, wrap: style.wrap,
|
||||
align: style.align || "left", paragraphHeight: style.paragraphHeight || 0, letterSpacing: style.letterSpacing || 0,
|
||||
fontChoice: font && !font.disabled ? font : state.textDefaults.fontChoice };
|
||||
@@ -953,7 +956,7 @@ async function toggleTextTool() {
|
||||
}
|
||||
|
||||
async function beginCanvasText(index, box, surface, page) {
|
||||
const font = state.textDefaults.fontChoice || state.textFonts[Number(el.textFont.value)];
|
||||
const font = state.textDefaults.fontChoice || state.textFonts[Number(fontPicker.value)];
|
||||
if (!font || font.disabled) {
|
||||
setStatus("尚未添加字体");
|
||||
el.textFontAdd.focus();
|
||||
@@ -984,19 +987,6 @@ function editorFonts(item) {
|
||||
return fonts;
|
||||
}
|
||||
|
||||
function setFontOptions(select, fonts, selected) {
|
||||
const key = (font) => font?.id || font?.postscriptName;
|
||||
select.replaceChildren();
|
||||
fonts.forEach((font, index) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = String(index);
|
||||
option.textContent = font.fullName || font.name;
|
||||
option.disabled = Boolean(font.disabled);
|
||||
select.append(option);
|
||||
});
|
||||
select.value = fonts.length ? String(Math.max(0, fonts.findIndex(font => key(font) === key(selected)))) : "";
|
||||
}
|
||||
|
||||
function updateTextFonts(item, refresh = false) {
|
||||
const id = item?.type === "TextObject" && !item.draft ? item.font : null;
|
||||
if (refresh || state.textFontID !== id) {
|
||||
@@ -1007,14 +997,14 @@ function updateTextFonts(item, refresh = false) {
|
||||
&& !state.textFonts.some(font => (font.id || font.postscriptName) === (selected.id || selected.postscriptName))) {
|
||||
state.textFonts.unshift(selected);
|
||||
}
|
||||
setFontOptions(el.textFont, state.textFonts, selected || state.textFonts[0]);
|
||||
if (!id) state.textDefaults.fontChoice = state.textFonts[Number(el.textFont.value)];
|
||||
fontPicker.setFonts(state.textFonts, selected || state.textFonts[0]);
|
||||
if (!id) state.textDefaults.fontChoice = state.textFonts[Number(fontPicker.value)];
|
||||
}
|
||||
}
|
||||
|
||||
async function changeTextFont() {
|
||||
const item = canvasEditor.selected;
|
||||
const font = state.textFonts[Number(el.textFont.value)];
|
||||
const font = state.textFonts[Number(fontPicker.value)];
|
||||
if (!state.ready || document.body.hasAttribute("aria-busy") || !font || font.disabled) return;
|
||||
const editing = canvasEditor.input;
|
||||
if (editing) {
|
||||
@@ -2992,7 +2982,7 @@ async function pasteEditorContent(event) {
|
||||
}
|
||||
const value = event.clipboardData.getData("text/plain").replace(/\t/g, " ");
|
||||
if (!value.trim()) return;
|
||||
const font = state.textFonts[Number(el.textFont.value)];
|
||||
const font = state.textFonts[Number(fontPicker.value)];
|
||||
if (!font || font.disabled) {
|
||||
setStatus("尚未添加字体");
|
||||
el.textFontAdd.focus();
|
||||
@@ -4459,7 +4449,7 @@ function updateObjectControls(item, reset = false) {
|
||||
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
||||
const text = item?.type === "TextObject" ? item : state.textDefaults;
|
||||
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(item?.items);
|
||||
el.textFont.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont");
|
||||
fontPicker.setDisabled(textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont"));
|
||||
el.textSize.disabled = textDisabled || Boolean(item?.draft) || text !== state.textDefaults && !canEditObject(item, "reflow");
|
||||
el.textColor.disabled = textDisabled || Boolean(item?.draft) || text !== state.textDefaults && !canEditObject(item, "update");
|
||||
el.textFontAdd.disabled = !state.editing || !state.ready || state.exporting;
|
||||
@@ -4495,12 +4485,12 @@ function updateObjectControls(item, reset = false) {
|
||||
}
|
||||
updateDrawingControls();
|
||||
const members = item?.items || (item ? [item] : []);
|
||||
const orders = members.map(member => member.order).sort((a, b) => a - b);
|
||||
const orders = members.map(member => member.position).sort((a, b) => a - b);
|
||||
const count = members[0]?.count || 0;
|
||||
const canRaise = orders.some((order, i) => order !== count - orders.length + i);
|
||||
const canLower = orders.some((order, i) => order !== i);
|
||||
el.objectOrder.disabled = disabled || cropping || count <= orders.length || !canEditObject(item, "order")
|
||||
|| members.some(member => member.layer !== members[0].layer);
|
||||
|| members.some(member => member.container !== members[0].container);
|
||||
for (const option of el.objectOrder.options) {
|
||||
option.disabled = el.objectOrder.disabled || (["up", "top"].includes(option.value) ? !canRaise : !canLower);
|
||||
}
|
||||
|
||||
@@ -156,9 +156,9 @@ export class CanvasEditor {
|
||||
viewer.addEventListener("lostpointercapture", () => this.cancel());
|
||||
viewer.addEventListener("keydown", (event) => this.keyDown(event));
|
||||
viewer.addEventListener("keyup", (event) => this.modifierChange(event));
|
||||
viewer.addEventListener("dblclick", () => {
|
||||
viewer.addEventListener("dblclick", (event) => {
|
||||
const item = this.selected;
|
||||
if (this.enabled && item && !item.items && !this.input && !this.options.busy()) {
|
||||
if (!event.altKey && this.enabled && item && !item.items && !this.input && !this.options.busy()) {
|
||||
this.options.onEdit(item);
|
||||
}
|
||||
});
|
||||
@@ -361,6 +361,10 @@ export class CanvasEditor {
|
||||
this.startShape(event);
|
||||
return;
|
||||
}
|
||||
if (event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.target.dataset.corner) {
|
||||
this.cycleSelection(event);
|
||||
return;
|
||||
}
|
||||
const node = event.target.closest(".edit-object, .edit-selection");
|
||||
if (node) {
|
||||
event.preventDefault();
|
||||
@@ -401,6 +405,19 @@ export class CanvasEditor {
|
||||
this.viewer.setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
cycleSelection(event) {
|
||||
const surface = event.target.closest(".page-surface");
|
||||
const page = this.pages.get(surface);
|
||||
if (!page) return;
|
||||
const point = pagePoint(event.clientX, event.clientY, surface.getBoundingClientRect(), page, this.options.rotation());
|
||||
const items = [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)).reverse()
|
||||
.filter(item => point.x >= item.x && point.x <= item.x + item.width && point.y >= item.y && point.y <= item.y + item.height);
|
||||
if (!items.length) return;
|
||||
event.preventDefault();
|
||||
this.viewer.focus({ preventScroll: true });
|
||||
this.select(items[(items.indexOf(this.selected) + 1) % items.length]);
|
||||
}
|
||||
|
||||
move(event) {
|
||||
const drag = this.drag;
|
||||
if (!drag || drag.pointerID !== event.pointerId) {
|
||||
|
||||
@@ -1,5 +1,141 @@
|
||||
const DATABASE = "ofdgo";
|
||||
|
||||
export class FontPicker {
|
||||
constructor(input, toggle, list, onChange) {
|
||||
Object.assign(this, { input, toggle, list, onChange, fonts: [], value: "", active: -1, open: false });
|
||||
input.addEventListener("input", () => this.show(input.value));
|
||||
input.addEventListener("focus", () => input.select());
|
||||
input.addEventListener("click", () => { if (!this.open) this.show(); });
|
||||
input.addEventListener("keydown", event => this.keyDown(event));
|
||||
input.addEventListener("blur", () => { this.commitExact(); this.close(); });
|
||||
toggle.addEventListener("pointerdown", event => event.preventDefault());
|
||||
toggle.addEventListener("click", () => {
|
||||
if (this.open) this.close();
|
||||
else { input.focus(); this.show(); }
|
||||
});
|
||||
list.addEventListener("pointerdown", event => {
|
||||
if (event.target.closest("[role=option]")) event.preventDefault();
|
||||
});
|
||||
list.addEventListener("click", event => {
|
||||
const option = event.target.closest("[role=option]");
|
||||
if (option) this.choose(Number(option.dataset.index));
|
||||
});
|
||||
window.addEventListener("resize", () => this.close());
|
||||
}
|
||||
|
||||
setFonts(fonts, selected) {
|
||||
this.close();
|
||||
this.fonts = fonts;
|
||||
const key = font => font?.id || font?.postscriptName;
|
||||
this.value = fonts.length ? String(Math.max(0, fonts.findIndex(font => key(font) === key(selected)))) : "";
|
||||
this.restore();
|
||||
}
|
||||
|
||||
setDisabled(disabled) {
|
||||
this.input.disabled = this.toggle.disabled = disabled;
|
||||
if (disabled) this.close();
|
||||
}
|
||||
|
||||
restore() {
|
||||
const font = this.fonts[Number(this.value)];
|
||||
this.input.value = font?.fullName || font?.name || "";
|
||||
this.input.setAttribute("aria-expanded", String(this.open));
|
||||
this.input.removeAttribute("aria-activedescendant");
|
||||
}
|
||||
|
||||
show(query = "") {
|
||||
if (this.input.disabled) return;
|
||||
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.postscriptName].filter(Boolean).join(" "));
|
||||
return terms.every(term => name.includes(term));
|
||||
});
|
||||
this.list.replaceChildren();
|
||||
for (const { font, index } of this.matches) {
|
||||
const option = document.createElement("div");
|
||||
option.id = `${this.list.id}-${index}`;
|
||||
option.dataset.index = String(index);
|
||||
option.setAttribute("role", "option");
|
||||
option.setAttribute("aria-disabled", String(Boolean(font.disabled)));
|
||||
option.textContent = font.fullName || font.name;
|
||||
this.list.append(option);
|
||||
}
|
||||
if (!this.matches.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "font-search-empty";
|
||||
empty.textContent = "无匹配字体";
|
||||
this.list.append(empty);
|
||||
}
|
||||
if (!this.open) this.list.showPopover();
|
||||
this.open = true;
|
||||
this.input.setAttribute("aria-expanded", "true");
|
||||
this.position();
|
||||
const selected = this.matches.findIndex(({ font, index }) => !font.disabled && String(index) === this.value);
|
||||
this.activate(selected < 0 ? this.matches.findIndex(({ font }) => !font.disabled) : selected);
|
||||
}
|
||||
|
||||
position() {
|
||||
if (!this.open) return;
|
||||
const rect = this.input.getBoundingClientRect();
|
||||
if (rect.right <= 0 || rect.left >= window.innerWidth) { this.close(); return; }
|
||||
const width = Math.min(Math.max(rect.width, 260), window.innerWidth - 16);
|
||||
Object.assign(this.list.style, { left: `${Math.max(8, Math.min(rect.left, window.innerWidth - width - 8))}px`,
|
||||
top: `${rect.bottom + 4}px`, width: `${width}px`, maxHeight: `${Math.max(40, Math.min(280, window.innerHeight - rect.bottom - 12))}px` });
|
||||
}
|
||||
|
||||
activate(index) {
|
||||
this.active = index;
|
||||
[...this.list.children].forEach((option, i) => option.setAttribute("aria-selected", String(i === index)));
|
||||
const option = this.list.children[index];
|
||||
if (option) {
|
||||
this.input.setAttribute("aria-activedescendant", option.id);
|
||||
option.scrollIntoView({ block: "nearest" });
|
||||
} else this.input.removeAttribute("aria-activedescendant");
|
||||
}
|
||||
|
||||
choose(index) {
|
||||
if (!this.fonts[index] || this.fonts[index].disabled) return;
|
||||
const changed = this.value !== String(index);
|
||||
this.value = String(index);
|
||||
this.close();
|
||||
if (changed) return this.onChange();
|
||||
}
|
||||
|
||||
commitExact() {
|
||||
const value = this.input.value.trim().toLocaleLowerCase();
|
||||
const index = this.fonts.findIndex(font => !font.disabled
|
||||
&& [font.fullName, font.name, font.postscriptName].some(name => name?.toLocaleLowerCase() === value));
|
||||
if (index >= 0) this.choose(index);
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.open) { this.open = false; this.list.hidePopover(); }
|
||||
this.restore();
|
||||
}
|
||||
|
||||
keyDown(event) {
|
||||
if (event.isComposing) return;
|
||||
if (!["Escape", "Enter", "ArrowDown", "ArrowUp", "Tab"].includes(event.key)) return;
|
||||
event.stopPropagation();
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
this.close();
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
if (this.open && this.active >= 0) this.choose(this.matches[this.active].index);
|
||||
else { this.commitExact(); this.close(); }
|
||||
} else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
if (!this.open) { this.show(); return; }
|
||||
const direction = event.key === "ArrowDown" ? 1 : -1;
|
||||
for (let i = this.active + direction; i >= 0 && i < this.matches.length; i += direction) {
|
||||
if (!this.matches[i].font.disabled) { this.activate(i); break; }
|
||||
}
|
||||
} else if (event.key === "Tab") { this.commitExact(); this.close(); }
|
||||
}
|
||||
}
|
||||
|
||||
export class FontManager {
|
||||
constructor({ onChange, onPermissionChange }) {
|
||||
this.userFonts = [];
|
||||
|
||||
Reference in new issue
Block a user