feat(编辑能力): 优化富文本编辑体验
build.yaml / build (push) Successful in 53s

This commit is contained in:
xiaoqidun committed 2026-10-14 14:45:23 +08:00
1 parent 61cfb79f25
commit 6299c0f8f9
4 files changed
+46 -17

No files matched your search

+1 -1
View File
@@ -118,7 +118,7 @@
<button id="textUnderline" class="button text-format-underline" data-text-format="underline" type="button" title="下划线" aria-label="下划线" aria-pressed="false" disabled>U</button>
<button id="textStrikeout" class="button text-format-strikeout" data-text-format="strikeout" type="button" title="删除线" aria-label="删除线" aria-pressed="false" disabled>S</button>
<button id="textShadow" class="button text-format-shadow" data-text-format="shadow" type="button" title="阴影" aria-label="阴影" aria-pressed="false" disabled>A</button>
<div class="text-highlight"><button id="textHighlight" class="button" data-text-format="background" type="button" title="突出显示" aria-label="突出显示" aria-pressed="false" disabled><span aria-hidden="true">A</span></button><input id="textBackground" class="color-input" type="color" value="#ffff00" title="突出显示颜色" aria-label="突出显示颜色" disabled></div>
<div class="text-highlight"><button id="textHighlight" class="button" data-text-format="background" type="button" title="底色" aria-label="底色" aria-pressed="false" disabled><span aria-hidden="true">A</span></button><input id="textBackground" class="color-input" type="color" value="#ffff00" title="选择底色" aria-label="选择底色" disabled></div>
<input id="textColor" class="color-input" type="color" value="#000000" aria-label="文字颜色" title="文字颜色" disabled>
<span>文字</span>
</div>
+4
View File
@@ -673,6 +673,10 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
onPreviewText: (item, value, font, size) => callWASM("ofdgoPreviewText", item.index, item.id, value, font || null, size),
onCommitRichText: saveRichText,
onPreviewRichText: validateRichText,
onTextError: (message, previous) => {
if (message) setStatus(message);
else if (el.statusText.textContent === previous) setStatus(state.doc ? pageStatus(state.pageIndex, state.doc.pageCount) : STATUS.ready);
},
onCommitText: (item, value, fontData, color = null) => item.draft
? changeDocument("ofdgoInsertText", null, item.index, value, fontData || item.fontData, item.x, item.y, item.size, item.color, item.width, item.wrap, item.align, item.paragraphHeight, item.letterSpacing, ...textIndents(item))
: changeDocument("ofdgoUpdateText", item, value, fontData || null, item.size, color),
+40 -15
View File
@@ -150,12 +150,10 @@ export class RichTextSession {
try {
await this.editor.options.onPreviewRichText(editing.item, this.model, editing.frame.width, this.fonts);
if (!current()) return;
editing.input.removeAttribute("aria-invalid");
editing.input.removeAttribute("title");
this.editor.setTextError(null);
} catch (error) {
if (!current()) return;
editing.input.setAttribute("aria-invalid", "true");
editing.input.title = error.message;
this.editor.setTextError(error);
}
}, 300);
}
@@ -271,6 +269,7 @@ export class RichTextSession {
line.append(document.createElement("br"));
}
input.replaceChildren(fragment);
this.editor.fitTextInput();
this.editing.range = null;
input.focus({ preventScroll: true });
restoreTextSelection(positions, this.caret);
@@ -1485,16 +1484,12 @@ export class CanvasEditor {
else paragraph.append(document.createElement("br"));
input.append(paragraph);
}
const left = item.leftIndent || 0, right = item.rightIndent || 0;
const left = item.leftIndent || 0;
const frame = { ...(item.textFrame || { width: item.width, height: item.height, matrix: [1, 0, 0, 1, item.x, item.y] }) };
if (item.wrap && item.paragraphWidth != null) frame.width = item.paragraphWidth;
const [a, b, c, d, e, f] = frame.matrix;
const autoSize = !source && !item.wrap && (!item.align || item.align === "left");
Object.assign(input.style, {
left: "0px", top: "0px",
width: autoSize ? "max-content" : `${(frame.width - left - right) * PX_PER_MM + 4}px`,
minWidth: autoSize ? "calc(1em + 4px)" : "0px",
minHeight: autoSize ? "0px" : `${frame.height * PX_PER_MM + 4}px`,
transform: `matrix(${a},${b},${c},${d},${(e + a * left) * PX_PER_MM},${(f + b * left) * PX_PER_MM})`,
whiteSpace: item.wrap ? "pre-wrap" : "pre",
unicodeBidi: "plaintext",
@@ -1512,6 +1507,7 @@ export class CanvasEditor {
item.artwork?.classList.add("edit-text-source");
item.node.classList.add("edit-text-source");
item.surface.append(input);
this.fitTextInput();
if (source) this.paintSourceText(source);
input.addEventListener("input", () => {
editing.rich?.readInput();
@@ -1609,6 +1605,36 @@ export class CanvasEditor {
else this.restoreSourceText(direction);
}
fitTextInput() {
const editing = this.input;
if (!editing || editing.source) return;
const { input, frame, item, rich } = editing;
const layout = rich?.model.layout || item;
const width = Math.max(0, frame.width - (layout.leftIndent || 0) - (layout.rightIndent || 0)) * PX_PER_MM + 4;
const autoWidth = !layout.align || layout.align === "left";
Object.assign(input.style, {
width: autoWidth ? "max-content" : `${width}px`,
maxWidth: layout.wrap ? `${width}px` : "none",
minWidth: `${Math.min(width, (rich?.typing.size || editing.style?.size || item.size) * PX_PER_MM + 4)}px`,
minHeight: "0px", height: "auto",
});
}
setTextError(error) {
const editing = this.input;
if (!editing) return;
const previous = editing.error || "";
editing.error = error ? String(error.message || error) : "";
if (editing.error) {
editing.input.setAttribute("aria-invalid", "true");
editing.input.title = editing.error;
} else {
editing.input.removeAttribute("aria-invalid");
editing.input.removeAttribute("title");
}
if (editing.error || previous) this.options.onTextError?.(editing.error, previous);
}
promoteText(rich) {
const editing = this.input;
if (!editing || editing.rich || editing.composing || editing.saving) return false;
@@ -1691,13 +1717,11 @@ export class CanvasEditor {
try {
const source = await this.options.onPreviewText(editing.item, value, font, size);
if (!current()) return;
editing.input.removeAttribute("aria-invalid");
editing.input.removeAttribute("title");
this.setTextError(null);
this.paintSourceText(source);
} catch (err) {
if (!current()) return;
editing.input.setAttribute("aria-invalid", "true");
editing.input.title = err.message;
this.setTextError(err);
}
}
@@ -1783,6 +1807,7 @@ export class CanvasEditor {
const item = { ...editing.item, ...editing.style };
editing.input.style.fontSize = `${item.size * PX_PER_MM}px`;
editing.input.style.color = item.color;
this.fitTextInput();
this.options.onTextChange();
if ("size" in values) this.previewSourceText();
}
@@ -1817,8 +1842,7 @@ export class CanvasEditor {
saved = editing.rich ? await this.options.onCommitRichText(editing.item, editing.rich.model, editing.rich.fonts)
: await this.options.onCommitText(editing.style ? { ...editing.item, ...editing.style } : editing.item, value, editing.fontData, editing.style?.color ?? null);
} catch (error) {
editing.input.setAttribute("aria-invalid", "true");
editing.input.title = error.message;
if (this.input === editing) this.setTextError(error);
}
if (this.input !== editing) {
return Boolean(saved);
@@ -1835,6 +1859,7 @@ export class CanvasEditor {
closeText() {
const editing = this.input;
this.setTextError(null);
this.input = null;
if (editing) {
editing.rich?.destroy();
+1 -1
View File
@@ -103,7 +103,7 @@ export class FontPicker {
if (!this.open) return;
const rect = this.input.parentElement.getBoundingClientRect();
if (rect.right <= 0 || rect.left >= window.innerWidth) { this.close(); return; }
const width = Math.min(Math.max(rect.width, 300), window.innerWidth - 16);
const width = Math.min(rect.width, 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` });
}