mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
ce05fe96af
commit
87457c4994
10 files changed
+529
-38
No files matched your search
@@ -1655,6 +1655,7 @@ button.outline-link:hover {
|
||||
.edit-handle {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: calc(9px / var(--surface-scale));
|
||||
height: calc(9px / var(--surface-scale));
|
||||
border: calc(1px / var(--surface-scale)) solid var(--accent);
|
||||
|
||||
@@ -433,6 +433,7 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
onDrawText: beginCanvasText,
|
||||
onDrawAnnotation: placeAnnotation,
|
||||
onDrawInk: (index, points, style, pressure) => changeDocument("ofdgoInsertInk", null, index, JSON.stringify(points), style.lineWidth, pressure, style.strokeColor),
|
||||
onPreviewText: (item, value, font, size) => callWASM("ofdgoPreviewText", item.index, item.id, value, font || null, size),
|
||||
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),
|
||||
@@ -1637,7 +1638,7 @@ async function editCanvasObject(item) {
|
||||
return;
|
||||
}
|
||||
if (item.type === "TextObject") {
|
||||
if (!confirmTextReflow(item)) return;
|
||||
if (!canEditObject(item, "textContent")) return;
|
||||
} else if (!canEditObject(item, item.type === "ImageObject" ? "replaceImage" : "update")) return;
|
||||
if (item.type === "PathObject") {
|
||||
el.shapeWidth.focus();
|
||||
@@ -1652,8 +1653,9 @@ async function editCanvasObject(item) {
|
||||
const data = await callWASM("ofdgoEditorFont", item.font);
|
||||
const face = new FontFace(`ofdgo-edit-${item.font}`, data.bytes);
|
||||
await face.load();
|
||||
const source = canEditObject(item, "layoutKnown") ? null : await callWASM("ofdgoPreviewText", item.index, item.id, item.text);
|
||||
if (openSeq === state.openSeq && item.node.isConnected) {
|
||||
canvasEditor.editText(item, face);
|
||||
canvasEditor.editText(item, face, source);
|
||||
}
|
||||
} catch (err) {
|
||||
if (openSeq === state.openSeq) {
|
||||
@@ -1828,7 +1830,7 @@ async function beginCanvasText(index, box, surface, page) {
|
||||
function editorFonts(item) {
|
||||
const fonts = fontManager.editorFonts();
|
||||
if (item) {
|
||||
fonts.unshift({ id: `embedded:${item.font}`, name: item.fontName || item.font, embedded: true, disabled: !canEditObject(item, "update") });
|
||||
fonts.unshift({ id: `embedded:${item.font}`, name: item.fontName || item.font, embedded: true, disabled: !canEditObject(item, "textContent") });
|
||||
}
|
||||
return fonts;
|
||||
}
|
||||
@@ -2712,7 +2714,7 @@ async function loadWASM() {
|
||||
if (data.ok) {
|
||||
request.resolve(data.data);
|
||||
} else {
|
||||
const err = new Error(missingGlyphMessage(data.missingGlyphs) || data.error);
|
||||
const err = new Error(missingGlyphMessage(data.missingGlyphs) || (data.reasonCode === "layoutRequired" ? "请先设置段落排版,再跨段或换行修改" : data.error));
|
||||
if (data.canceled) {
|
||||
err.name = "AbortError";
|
||||
}
|
||||
@@ -4710,7 +4712,7 @@ async function selectSearchMatch(index) {
|
||||
if (state.editing) {
|
||||
const node = [...pageShell(match.page).querySelectorAll(".edit-object")].find(node => canvasEditor.nodes.get(node)?.id === match.id);
|
||||
const item = node && canvasEditor.nodes.get(node);
|
||||
if (item?.type === "TextObject" && canEditObject(item, "update")) {
|
||||
if (item?.type === "TextObject") {
|
||||
state.selectObjects = true;
|
||||
setPan(false);
|
||||
canvasEditor.setTool("");
|
||||
@@ -6036,7 +6038,7 @@ function updateObjectControls(item, reset = false) {
|
||||
el.resetCropButton.disabled = canEditObject(item, "resetCrop") ? disabled : el.cropImageButton.disabled || !cropping && (item.scoped ? !item.cropped : !item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
||||
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
||||
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
||||
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
||||
|| !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
||||
el.editObjectButton.textContent = canEditObject(item, "enter") ? "进入" : item?.type === "ImageObject" ? "替换" : "修改";
|
||||
el.editObjectButton.title = canEditObject(item, "enter") ? item?.type === "Annotation" ? "进入注解" : "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
|
||||
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
|
||||
|
||||
+153
-5
@@ -11,6 +11,31 @@ export function editTextValue(input) {
|
||||
return text;
|
||||
}
|
||||
|
||||
function editTextSelection(input, saved) {
|
||||
const selection = document.getSelection();
|
||||
const points = saved ? [[saved.startContainer, saved.startOffset], [saved.endContainer, saved.endOffset]]
|
||||
: selection?.rangeCount ? [[selection.anchorNode, selection.anchorOffset], [selection.focusNode, selection.focusOffset]] : null;
|
||||
if (!points || points.some(([node]) => !input.contains(node))) return null;
|
||||
return points.map(([node, position]) => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(input);
|
||||
range.setEnd(node, position);
|
||||
return editTextValue(range.cloneContents()).length;
|
||||
});
|
||||
}
|
||||
|
||||
function restoreTextSelection(positions, caret, saved) {
|
||||
if (!caret || !positions.length) return;
|
||||
const endpoints = caret.map(point => {
|
||||
const entry = positions.find(entry => entry.end >= point) || positions.at(-1);
|
||||
return [entry.node, Math.max(0, Math.min(point - entry.start, entry.node.length))];
|
||||
});
|
||||
if (saved) {
|
||||
saved.setStart(...endpoints[0]);
|
||||
saved.setEnd(...endpoints[1]);
|
||||
} else document.getSelection().setBaseAndExtent(...endpoints[0], ...endpoints[1]);
|
||||
}
|
||||
|
||||
export function pagePoint(x, y, rect, page, rotation) {
|
||||
const u = (x - rect.left) / rect.width;
|
||||
const v = (y - rect.top) / rect.height;
|
||||
@@ -1145,7 +1170,7 @@ export class CanvasEditor {
|
||||
}
|
||||
}
|
||||
|
||||
editText(item, face) {
|
||||
editText(item, face, source = null) {
|
||||
this.cancel();
|
||||
this.closeText();
|
||||
this.select(item);
|
||||
@@ -1178,17 +1203,28 @@ export class CanvasEditor {
|
||||
letterSpacing: `${(item.letterSpacing || 0) * PX_PER_MM}px`,
|
||||
textIndent: `${(item.firstLineIndent || 0) * PX_PER_MM}px`,
|
||||
});
|
||||
this.input = { input, item, face, fontChoice: item.fontChoice };
|
||||
this.input = { input, item, face, source: Boolean(source), fontChoice: item.fontChoice };
|
||||
const editing = this.input;
|
||||
if (source) editing.history = { entries: [{ value: item.text }], index: 0 };
|
||||
item.artwork?.classList.add("edit-text-source");
|
||||
item.node.classList.add("edit-text-source");
|
||||
item.surface.append(input);
|
||||
input.addEventListener("input", () => this.options.onTextChange());
|
||||
input.addEventListener("compositionstart", () => { editing.composing = true; });
|
||||
if (source) this.paintSourceText(source);
|
||||
input.addEventListener("input", () => {
|
||||
this.recordSourceText();
|
||||
this.options.onTextChange();
|
||||
this.previewSourceText();
|
||||
});
|
||||
input.addEventListener("compositionstart", () => {
|
||||
this.rememberSourceSelection();
|
||||
editing.composing = true;
|
||||
});
|
||||
input.addEventListener("compositionend", () => {
|
||||
editing.composing = false;
|
||||
this.recordSourceText();
|
||||
Promise.resolve().then(() => {
|
||||
if (this.input === editing && editing.commitAfterComposition) this.commitText();
|
||||
else this.previewSourceText();
|
||||
});
|
||||
});
|
||||
input.addEventListener("paste", event => {
|
||||
@@ -1196,6 +1232,12 @@ export class CanvasEditor {
|
||||
document.execCommand("insertText", false, event.clipboardData.getData("text/plain"));
|
||||
});
|
||||
input.addEventListener("beforeinput", event => {
|
||||
if (editing.source && (event.inputType === "historyUndo" || event.inputType === "historyRedo")) {
|
||||
event.preventDefault();
|
||||
this.restoreSourceText(event.inputType === "historyUndo" ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
this.rememberSourceSelection();
|
||||
if (event.inputType.startsWith("format")) event.preventDefault();
|
||||
if (event.inputType === "insertLineBreak") {
|
||||
event.preventDefault();
|
||||
@@ -1221,7 +1263,10 @@ export class CanvasEditor {
|
||||
if (event.isComposing || editing.composing || editing.saving) {
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
if (editing.source && (event.ctrlKey || event.metaKey) && !event.altKey && ["z", "y"].includes(event.key.toLowerCase())) {
|
||||
event.preventDefault();
|
||||
this.restoreSourceText(event.key.toLowerCase() === "y" || event.shiftKey ? 1 : -1);
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
this.closeText();
|
||||
this.focus();
|
||||
@@ -1234,6 +1279,107 @@ export class CanvasEditor {
|
||||
if (!item.draft) document.getSelection().selectAllChildren(input);
|
||||
}
|
||||
|
||||
rememberSourceSelection() {
|
||||
const editing = this.input;
|
||||
if (editing?.history && !editing.composing) editing.history.entries[editing.history.index].caret = editTextSelection(editing.input);
|
||||
}
|
||||
|
||||
recordSourceText() {
|
||||
const editing = this.input;
|
||||
if (!editing?.history || editing.composing) return;
|
||||
const { history, input } = editing, value = editTextValue(input);
|
||||
if (history.entries[history.index].value === value) return;
|
||||
history.entries.splice(++history.index, Infinity, { value, caret: editTextSelection(input) });
|
||||
}
|
||||
|
||||
restoreSourceText(direction) {
|
||||
const editing = this.input, history = editing?.history;
|
||||
if (!history || editing.composing || editing.saving) return;
|
||||
const index = history.index + direction;
|
||||
if (index < 0 || index >= history.entries.length) return;
|
||||
history.index = index;
|
||||
const entry = history.entries[index], fragment = document.createDocumentFragment(), positions = [];
|
||||
let position = 0;
|
||||
for (const text of entry.value.split("\n")) {
|
||||
const paragraph = document.createElement("div");
|
||||
const node = document.createTextNode(text);
|
||||
paragraph.append(node);
|
||||
positions.push({ node, start: position, end: position + text.length });
|
||||
if (!text) paragraph.append(document.createElement("br"));
|
||||
fragment.append(paragraph);
|
||||
position += text.length + 1;
|
||||
}
|
||||
editing.input.replaceChildren(fragment);
|
||||
restoreTextSelection(positions, entry.caret);
|
||||
this.options.onTextChange();
|
||||
this.previewSourceText();
|
||||
}
|
||||
|
||||
async previewSourceText() {
|
||||
const editing = this.input;
|
||||
if (!editing?.source || editing.composing || editing.saving) return;
|
||||
const value = editTextValue(editing.input), font = editing.fontData, size = editing.style?.size || editing.item.size;
|
||||
const current = () => this.input === editing && !editing.composing && !editing.saving && editing.fontData === font
|
||||
&& (editing.style?.size || editing.item.size) === size && editTextValue(editing.input) === value;
|
||||
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.paintSourceText(source);
|
||||
} catch (err) {
|
||||
if (!current()) return;
|
||||
editing.input.setAttribute("aria-invalid", "true");
|
||||
editing.input.title = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
paintSourceText(source) {
|
||||
const editing = this.input, { input, item, face } = editing;
|
||||
const caret = editTextSelection(input, editing.range);
|
||||
const size = (editing.style?.size || item.size) * PX_PER_MM;
|
||||
const context = document.createElement("canvas").getContext("2d");
|
||||
context.font = `${source.italic ? "italic" : "normal"} ${source.weight || 400} ${size}px "${face.family}"`;
|
||||
const metrics = context.measureText("Mg"), baseline = (size + metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent) / 2;
|
||||
Object.assign(input.style, { lineHeight: `${size}px`, fontWeight: String(source.weight || 400), fontStyle: source.italic ? "italic" : "normal" });
|
||||
const fragment = document.createDocumentFragment(), positions = [];
|
||||
let line, lineIndex = -1, naturalX = 0, position = 0, width = 0, height = 0;
|
||||
for (const run of source.runs) {
|
||||
if (!line || run.line) {
|
||||
if (line) position++;
|
||||
line = document.createElement("div");
|
||||
fragment.append(line);
|
||||
lineIndex++;
|
||||
naturalX = 0;
|
||||
}
|
||||
let x = run.x * PX_PER_MM, y = run.y * PX_PER_MM, index = 0;
|
||||
for (const char of run.text) {
|
||||
const span = document.createElement("span"), node = document.createTextNode(char);
|
||||
span.append(node);
|
||||
Object.assign(span.style, { display: "inline-block", opacity: String((item.alpha ?? 255) / 255), transformOrigin: "left top", transform: `translate(${x - naturalX}px,${y - baseline - lineIndex * size}px) scaleX(${source.scale})` });
|
||||
line.append(span);
|
||||
positions.push({ node, start: position, end: position + char.length });
|
||||
position += char.length;
|
||||
const advance = context.measureText(char).width;
|
||||
width = Math.max(width, x + advance * source.scale);
|
||||
height = Math.max(height, y + size - baseline);
|
||||
naturalX += advance;
|
||||
x += run.dx?.length ? run.dx[Math.min(index, run.dx.length - 1)] * PX_PER_MM : run.dy?.length ? 0 : advance * source.scale;
|
||||
y += run.dy?.length ? run.dy[Math.min(index, run.dy.length - 1)] * PX_PER_MM : 0;
|
||||
index++;
|
||||
}
|
||||
if (!line.hasChildNodes()) {
|
||||
const node = document.createTextNode("");
|
||||
line.append(node, document.createElement("br"));
|
||||
positions.push({ node, start: position, end: position });
|
||||
}
|
||||
}
|
||||
input.replaceChildren(fragment);
|
||||
input.style.width = `${Math.max(parseFloat(input.style.width), width + 4)}px`;
|
||||
input.style.minHeight = `${Math.max(parseFloat(input.style.minHeight), height + 4)}px`;
|
||||
restoreTextSelection(positions, caret, editing.range);
|
||||
}
|
||||
|
||||
setTextFont(face, data) {
|
||||
const editing = this.input;
|
||||
document.fonts.add(face);
|
||||
@@ -1243,6 +1389,7 @@ export class CanvasEditor {
|
||||
editing.input.style.fontFamily = `"${face.family}"`;
|
||||
this.options.onTextChange();
|
||||
editing.input.focus({ preventScroll: true });
|
||||
this.previewSourceText();
|
||||
}
|
||||
|
||||
setTextStyle(values) {
|
||||
@@ -1254,6 +1401,7 @@ export class CanvasEditor {
|
||||
editing.input.style.fontSize = `${item.size * PX_PER_MM}px`;
|
||||
editing.input.style.color = item.color;
|
||||
this.options.onTextChange();
|
||||
if ("size" in values) this.previewSourceText();
|
||||
}
|
||||
|
||||
textChanged() {
|
||||
|
||||
Reference in new issue
Block a user