feat(编辑能力): 统一文字格式与段落编辑
build.yaml / build (push) Successful in 46s

This commit is contained in:
xiaoqidun committed 2026-10-28 14:10:32 +08:00
1 parent e5658de69d
commit c2b0339fdf
5 files changed
+174 -100

No files matched your search

+52 -70
View File
@@ -874,7 +874,7 @@ el.textLineHeight.addEventListener("change", () => changeParagraph(undefined, "l
editorClick(el.paragraphButton, () => { editorClick(el.paragraphButton, () => {
const item = currentTextStyle(); const item = currentTextStyle();
for (const key of ["leftIndent", "rightIndent", "firstLineIndent"]) { for (const key of ["leftIndent", "rightIndent", "firstLineIndent"]) {
const mixed = Boolean(canvasEditor.input?.rich || item.richText) && item[key] === undefined; const mixed = Boolean(canvasEditor.input?.rich || item.richText || item.items) && item[key] === undefined;
el[key].required = !mixed; el[key].required = !mixed;
el[key].placeholder = mixed ? "混合" : ""; el[key].placeholder = mixed ? "混合" : "";
el[key].value = mixed ? "" : item[key] || 0; el[key].value = mixed ? "" : item[key] || 0;
@@ -1197,10 +1197,7 @@ el.paragraphForm.addEventListener("submit", async event => {
} else if (item === state.textDefaults) { } else if (item === state.textDefaults) {
Object.assign(item, indents); Object.assign(item, indents);
} else { } else {
if (!confirmTextReflow(item)) return; const saved = await saveTextParagraphs(item, indents);
const saved = item.richText ? await saveRichParagraphs(item, indents)
: await changeDocument("ofdgoLayoutText", item, null, null, Boolean(item.wrap), item.align || "left",
item.paragraphHeight || 0, item.letterSpacing || 0, ...textIndents(indents));
if (!saved) return; if (!saved) return;
} }
if (isForegroundDocument(tab)) el.paragraphPanel.close(); if (isForegroundDocument(tab)) el.paragraphPanel.close();
@@ -4163,13 +4160,15 @@ async function loadRichText(item, firstFace, tab = documentTab(), firstData = nu
const fontData = remaining.runs.length ? await callWASM("ofdgoRichTextFonts", JSON.stringify(remaining)) : []; const fontData = remaining.runs.length ? await callWASM("ofdgoRichTextFonts", JSON.stringify(remaining)) : [];
const resources = new Map(fontData.map(font => [font.key, font.data])); const resources = new Map(fontData.map(font => [font.key, font.data]));
if (firstData) resources.set(item.font, firstData); if (firstData) resources.set(item.font, firstData);
const faces = new Map(); const faces = new Map(), pending = [...new Set(model.runs.map(run => run.format.font))];
for (const id of new Set(model.runs.map(run => run.format.font))) { await Promise.all(Array.from({ length: Math.min(4, pending.length) }, async () => {
const data = id === item.font && firstFace ? null : resources.get(id); while (pending.length) {
const id = pending.shift(), data = id === item.font && firstFace ? null : resources.get(id);
const face = data ? await new FontFace(`ofdgo-rich-${crypto.randomUUID()}`, data, fontFaceStyle(data)).load() : firstFace; const face = data ? await new FontFace(`ofdgo-rich-${crypto.randomUUID()}`, data, fontFaceStyle(data)).load() : firstFace;
if (tab.closed || wasmSeq !== tab.state.wasmSeq) throw new Error("文档已更改"); if (tab.closed || wasmSeq !== tab.state.wasmSeq) throw new Error("文档已更改");
faces.set(id, face); faces.set(id, face);
} }
}));
return { model, faces, resources, frame: prepared.frame, reflow }; return { model, faces, resources, frame: prepared.frame, reflow };
} }
@@ -4216,10 +4215,12 @@ async function changeRangeFormat(key, value) {
updateObjectControls(null); return; updateObjectControls(null); return;
} }
const item = canvasEditor.selected; const item = canvasEditor.selected;
if (!canvasEditor.input && (item?.items || item?.scoped)) { if (!canvasEditor.input && item) {
const reflow = !canEditObject(item, "layoutKnown");
if (reflow && !confirmTextReflow(item)) return;
const current = currentTextStyle()[key]; const current = currentTextStyle()[key];
const patch = { [key]: value ?? (key === "background" ? current ? "" : textBackground.value : current !== true) }; const patch = { [key]: value ?? (key === "background" ? current ? "" : textBackground.value : current !== true) };
await changeDocument("ofdgoFormatText", { ...item, id: (item.items || [item]).map(member => member.id) }, JSON.stringify(patch)); await changeDocument("ofdgoFormatText", { ...item, id: (item.items || [item]).map(member => member.id) }, JSON.stringify(patch), reflow);
return; return;
} }
if (!await ensureRichInput()) return; if (!await ensureRichInput()) return;
@@ -4278,15 +4279,13 @@ async function validateRichText(item, model, width, fonts = []) {
} }
} }
async function saveRichText(item, model, fonts = [], box = item) { function saveRichText(item, model, fonts = [], box = item) {
const tab = documentTab(), openSeq = state.openSeq, wasmSeq = state.wasmSeq;
const boundary = { x: box.x, y: box.y, width: box.width, height: box.height }; const boundary = { x: box.x, y: box.y, width: box.width, height: box.height };
if (item.richReflow) boundary.reflow = true; if (item.richReflow) boundary.reflow = true;
if (box.frameWidth !== undefined) Object.assign(boundary, { frameWidth: box.frameWidth, frameOffset: box.frameOffset || 0 }); if (box.frameWidth !== undefined) Object.assign(boundary, { frameWidth: box.frameWidth, frameOffset: box.frameOffset || 0 });
fonts = await validateRichText(item, model, box.frameWidth ?? item.textFrame?.width ?? item.width, fonts);
if (tab.closed || !isForegroundDocument(tab) || tab.state.openSeq !== openSeq || tab.state.wasmSeq !== wasmSeq) return false;
return changeDocument("ofdgoSaveRichText", item.draft ? null : item, return changeDocument("ofdgoSaveRichText", item.draft ? null : item,
...(item.draft ? [item.index, ""] : []), JSON.stringify(model), boundary, fonts); ...(item.draft ? [item.index, ""] : []), JSON.stringify(model), boundary,
() => validateRichText(item, model, box.frameWidth ?? item.textFrame?.width ?? item.width, fonts));
} }
function textIndents(item) { function textIndents(item) {
@@ -4452,15 +4451,6 @@ async function changeTextFont() {
const item = selectedText(canvasEditor.selected); const item = selectedText(canvasEditor.selected);
const font = state.textFonts[Number(fontPicker.value)]; 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") || !font || font.disabled) return;
if (item?.richText && !canvasEditor.input) {
try {
await editCanvasObject(item);
if (!isForegroundDocument(tab) || openSeq !== state.openSeq || !canvasEditor.input?.rich) return;
} catch (error) {
if (isForegroundDocument(tab) && openSeq === state.openSeq) showError(error, false);
return;
}
}
const editing = canvasEditor.input; const editing = canvasEditor.input;
if (editing) { if (editing) {
if (editing.composing) return; if (editing.composing) return;
@@ -4595,10 +4585,7 @@ async function changeTextAlignment() {
if (!editing.rich && !editing.item.draft && !confirmTextReflow(editing.item)) return; if (!editing.rich && !editing.item.draft && !confirmTextReflow(editing.item)) return;
if (!await ensureRichInput(!canEditObject(editing.item, "layoutKnown")) || !isForegroundDocument(tab) || canvasEditor.input !== editing || editing.alignSequence !== sequence) return; if (!await ensureRichInput(!canEditObject(editing.item, "layoutKnown")) || !isForegroundDocument(tab) || canvasEditor.input !== editing || editing.alignSequence !== sequence) return;
editing.rich.align(align); editing.rich.align(align);
} else if (canvasEditor.selected?.richText) { } else await saveTextParagraphs(currentTextStyle(), { align });
const item = canvasEditor.selected;
await saveRichParagraphs(item, { align });
} else await changeParagraph();
} catch (error) { } catch (error) {
if (isForegroundDocument(tab) && canvasEditor.input === editing && (!editing || editing.alignSequence === sequence)) showError(error, false); if (isForegroundDocument(tab) && canvasEditor.input === editing && (!editing || editing.alignSequence === sequence)) showError(error, false);
} finally { } finally {
@@ -4606,10 +4593,14 @@ async function changeTextAlignment() {
} }
} }
async function saveRichParagraphs(item, patch) { async function saveTextParagraphs(item, patch) {
const { wrap, ...values } = patch, model = item.richText; if (!Object.keys(patch).length) return true;
return saveRichText(item, { ...model, layout: { ...model.layout, ...(wrap !== undefined ? { wrap } : {}) }, if (item === state.textDefaults) {
paragraphs: model.runs.map(run => run.text).join("").split("\n").map((_, i) => ({ align: "", ...model.paragraphs?.[i], ...values })) }); Object.assign(item, patch);
return true;
}
if (!confirmTextReflow(item)) return false;
return changeDocument("ofdgoLayoutTexts", item, JSON.stringify(patch));
} }
async function changeParagraph(wrap, changed) { async function changeParagraph(wrap, changed) {
@@ -4623,33 +4614,18 @@ async function changeParagraph(wrap, changed) {
} }
return changeInputParagraph(patch); return changeInputParagraph(patch);
} }
wrap ??= item.wrap;
if (document.body.hasAttribute("data-busy") || item.draft) return; if (document.body.hasAttribute("data-busy") || item.draft) return;
if (!el.textLineHeight.checkValidity()) { const patch = wrap !== undefined ? { wrap: Boolean(wrap) } : {};
updateObjectControls(item, true); for (const [input, key, original] of [[el.textLineHeight, "lineHeight", item.paragraphHeight], [el.textSpacing, "letterSpacing", item.letterSpacing]]) {
return; if (key !== changed) continue;
if (!input.checkValidity()) return updateObjectControls(item, true);
patch[key] = inputMillimeters(input, original || 0);
} }
const height = inputMillimeters(el.textLineHeight, item.paragraphHeight || 0);
if (!el.textSpacing.checkValidity()) {
updateObjectControls(canvasEditor.selected, true);
return;
}
const spacing = inputMillimeters(el.textSpacing, item.letterSpacing || 0);
if (item === state.textDefaults) { if (item === state.textDefaults) {
Object.assign(item, { wrap: Boolean(wrap), align: el.textAlign.value, paragraphHeight: height, letterSpacing: spacing }); if (patch.lineHeight !== undefined) item.paragraphHeight = patch.lineHeight;
updateObjectControls(canvasEditor.selected, true); if (patch.letterSpacing !== undefined) item.letterSpacing = patch.letterSpacing;
return; if (patch.wrap !== undefined) item.wrap = patch.wrap;
} } else await saveTextParagraphs(item, patch);
if (!confirmTextReflow(item)) {
updateObjectControls(item, true);
return;
}
try {
if (item.richText) await saveRichParagraphs(item, changed === "lineHeight" ? { lineHeight: height } : changed === "letterSpacing" ? { letterSpacing: spacing } : { wrap: Boolean(wrap) });
else await changeDocument("ofdgoLayoutText", item, null, null, Boolean(wrap), el.textAlign.value, height, spacing, ...textIndents(item));
} catch (error) {
if (isForegroundDocument(tab)) showError(error, false);
}
if (isForegroundDocument(tab)) updateObjectControls(canvasEditor.selected, true); if (isForegroundDocument(tab)) updateObjectControls(canvasEditor.selected, true);
} }
@@ -4679,7 +4655,7 @@ async function changeTextStyle(color) {
const size = color ? item.size : inputMillimeters(el.textSize, item.size); const size = color ? item.size : inputMillimeters(el.textSize, item.size);
const selectedColor = el.textColor.value; const selectedColor = el.textColor.value;
const fill = color && selectedColor !== item.color ? selectedColor : null; const fill = color && selectedColor !== item.color ? selectedColor : null;
if (canvasEditor.input || item.richText) { if (canvasEditor.input) {
const editing = canvasEditor.input, tab = documentTab(); const editing = canvasEditor.input, tab = documentTab();
try { try {
if (!await ensureRichInput() || !isForegroundDocument(tab) || editing && canvasEditor.input !== editing) return; if (!await ensureRichInput() || !isForegroundDocument(tab) || editing && canvasEditor.input !== editing) return;
@@ -4691,6 +4667,10 @@ async function changeTextStyle(color) {
updateObjectControls(canvasEditor.selected, true); updateObjectControls(canvasEditor.selected, true);
return; return;
} }
if (item.richText) {
await changeDocument("ofdgoFormatText", item, JSON.stringify(color ? { color: selectedColor } : { size }));
return;
}
if (item?.scoped && color) { if (item?.scoped && color) {
if (color && fill !== null && canEditObject(item, "paint")) { if (color && fill !== null && canEditObject(item, "paint")) {
await changeDocument("ofdgoCompositeTextColor", item, fill); await changeDocument("ofdgoCompositeTextColor", item, fill);
@@ -4813,7 +4793,7 @@ async function changeDocument(name, item, ...args) {
const toolbarFocus = el.editorTools.contains(active) ? active : null; const toolbarFocus = el.editorTools.contains(active) ? active : null;
const previous = pageView.currentPageInfo() || state.doc.pages[Math.min(state.pageIndex, state.doc.pages.length - 1)] || {}; const previous = pageView.currentPageInfo() || state.doc.pages[Math.min(state.pageIndex, state.doc.pages.length - 1)] || {};
const originalDocument = state.doc; const originalDocument = state.doc;
const before = editorLocation(), revision = state.editorInfo.revision; const before = editorLocation(), revision = state.editorInfo.revision, wasmSeq = state.wasmSeq;
const restoring = name === "ofdgoUndo" || name === "ofdgoRedo"; const restoring = name === "ofdgoUndo" || name === "ofdgoRedo";
const { scrollLeft, scrollTop } = el.viewerPanel; const { scrollLeft, scrollTop } = el.viewerPanel;
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments"; const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments";
@@ -4830,8 +4810,8 @@ async function changeDocument(name, item, ...args) {
} }
}, 250) : null; }, 250) : null;
try { try {
args = await Promise.all(args); args = await Promise.all(args.map(value => typeof value === "function" ? value() : value));
if (tab.closed) { if (tab.closed || state.wasmSeq !== wasmSeq || state.doc !== originalDocument || state.editorInfo?.revision !== revision) {
return; return;
} }
const scope = objectParent(item); const scope = objectParent(item);
@@ -4849,8 +4829,8 @@ async function changeDocument(name, item, ...args) {
const scopedRichText = scoped && name === "ofdgoSaveRichText"; const scopedRichText = scoped && name === "ofdgoSaveRichText";
if (scopedRichText) args.push(scope); if (scopedRichText) args.push(scope);
const textSelection = name === "ofdgoUpdateTextSelection"; const textSelection = name === "ofdgoUpdateTextSelection";
if (scoped && !operation && !textSelection && !scopedRichText) throw new Error("内部对象暂不支持此操作"); if (scoped && !operation && !textSelection && !scopedRichText && name !== "ofdgoLayoutTexts") throw new Error("内部对象暂不支持此操作");
const doc = ["ofdgoStyleText", "ofdgoFormatText"].includes(name) const doc = ["ofdgoStyleText", "ofdgoFormatText", "ofdgoLayoutTexts"].includes(name)
? await callWASM(name, item.index, ids, ...args) ? await callWASM(name, item.index, ids, ...args)
: textSelection : textSelection
? await callWASM(name, item.index, item.selectionIDs, ...args) ? await callWASM(name, item.index, item.selectionIDs, ...args)
@@ -4987,7 +4967,9 @@ async function changeDocument(name, item, ...args) {
if (name === "ofdgoChangeFontBinding") err.message = fontBindingError(err.message); if (name === "ofdgoChangeFontBinding") err.message = fontBindingError(err.message);
if (name === "ofdgoChangeInvoice") err.message = invoiceIssues([err.message]); if (name === "ofdgoChangeInvoice") err.message = invoiceIssues([err.message]);
if (isForegroundDocument(tab) && openSeq === state.openSeq) { if (isForegroundDocument(tab) && openSeq === state.openSeq) {
if (settingsElements.Panel.open) { if (name === "ofdgoSaveRichText" && canvasEditor.input?.saving) {
canvasEditor.setTextError(err);
} else if (settingsElements.Panel.open) {
settingsElements.Status.textContent = err.message; settingsElements.Status.textContent = err.message;
} else if (fontBindingElements.Panel.open) { } else if (fontBindingElements.Panel.open) {
fontBindingElements.Status.textContent = fontBindingError(err.message); fontBindingElements.Status.textContent = fontBindingError(err.message);
@@ -12553,22 +12535,22 @@ function updateObjectControls(item, reset = false) {
el.undoButton.disabled = !state.editorInfo?.canUndo || !state.ready || state.exporting; el.undoButton.disabled = !state.editorInfo?.canUndo || !state.ready || state.exporting;
el.redoButton.disabled = !state.editorInfo?.canRedo || !state.ready || state.exporting; el.redoButton.disabled = !state.editorInfo?.canRedo || !state.ready || state.exporting;
} }
el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items); el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled;
el.textWrap.disabled = el.textLineHeight.disabled || text === state.textDefaults; el.textWrap.disabled = el.textLineHeight.disabled || text === state.textDefaults;
const reflow = document.querySelector("#textReflow"), positioned = canvasEditor.input?.source && (item?.selectionIDs || !canEditObject(item, "layoutKnown")); const reflow = document.querySelector("#textReflow"), positioned = canvasEditor.input?.source && (item?.selectionIDs || !canEditObject(item, "layoutKnown"));
reflow.hidden = !positioned; reflow.hidden = !positioned;
el.textWrap.hidden = !!positioned; el.textWrap.hidden = !!positioned;
reflow.disabled = textDisabled || !canEditObject(item, "reflow") || !!canvasEditor.input?.reflow; reflow.disabled = textDisabled || !canEditObject(item, "reflow") || !!canvasEditor.input?.reflow;
el.paragraphButton.disabled = el.textLineHeight.disabled; el.paragraphButton.disabled = el.textLineHeight.disabled;
el.textAlign.disabled = el.textSize.disabled || Boolean(item?.items || item?.selectionIDs); el.textAlign.disabled = el.textSize.disabled || Boolean(item?.selectionIDs);
const layoutKnown = text === state.textDefaults || Boolean(item?.draft) || canEditObject(item, "layoutKnown"); const layoutKnown = text === state.textDefaults || Boolean(item?.draft) || canEditObject(item, "layoutKnown");
const rich = canvasEditor.input?.rich; const rich = canvasEditor.input?.rich;
const alignment = rich ? richTextAlignment(rich.model, ...rich.selection()) : item?.richText ? richTextAlignment(item.richText) : layoutKnown ? text.align || "left" : ""; const alignment = item?.items ? text.align ?? "" : rich ? richTextAlignment(rich.model, ...rich.selection()) : item?.richText ? richTextAlignment(item.richText) : layoutKnown ? text.align || "left" : "";
el.textAlign.options[0].textContent = rich || item?.richText ? "混合" : "原文"; el.textAlign.options[0].textContent = rich || item?.richText || item?.items ? "混合" : "原文";
el.textAlign.value = alignment; el.textAlign.value = alignment;
el.textLineHeight.placeholder = (rich || item?.richText) && text.paragraphHeight === undefined ? "混合" : layoutKnown ? "自动" : "原文"; el.textLineHeight.placeholder = (rich || item?.richText || item?.items) && text.paragraphHeight === undefined ? "混合" : layoutKnown ? "自动" : "原文";
el.textSpacing.placeholder = (rich || item?.richText) && text.letterSpacing === undefined ? "混合" : layoutKnown ? "0" : "原文"; el.textSpacing.placeholder = (rich || item?.richText || item?.items) && text.letterSpacing === undefined ? "混合" : layoutKnown ? "0" : "原文";
el.textWrap.setAttribute("data-pressed", String(Boolean(text.wrap))); el.textWrap.setAttribute("data-pressed", item?.items && text.wrap === undefined ? "mixed" : String(Boolean(text.wrap)));
if (reset || document.activeElement !== el.textLineHeight) { if (reset || document.activeElement !== el.textLineHeight) {
el.textLineHeight.value = text.paragraphHeight ? String(displayPoints(text.paragraphHeight)) : ""; el.textLineHeight.value = text.paragraphHeight ? String(displayPoints(text.paragraphHeight)) : "";
} }
+8
View File
@@ -754,6 +754,14 @@ export function selectedText(item) {
const value = formats[0][key]; const value = formats[0][key];
text[key] = formats.every(format => format[key] === value) ? value : undefined; text[key] = formats.every(format => format[key] === value) ? value : undefined;
} }
const layouts = item.items.map(member => member.richText ? { ...richTextLayout(member.richText), wrap: Boolean(member.richText.layout.wrap) } : {
align: member.align || "left", wrap: Boolean(member.wrap), lineHeight: member.paragraphHeight || 0,
letterSpacing: member.letterSpacing || 0, leftIndent: member.leftIndent || 0, rightIndent: member.rightIndent || 0, firstLineIndent: member.firstLineIndent || 0,
});
for (const key of ["align", "wrap", "lineHeight", "letterSpacing", "leftIndent", "rightIndent", "firstLineIndent"]) {
const values = layouts.map(layout => layout[key]);
text[key === "lineHeight" ? "paragraphHeight" : key] = values.every(value => value === values[0]) ? values[0] : undefined;
}
return text; return text;
} }
+34 -23
View File
@@ -4926,7 +4926,7 @@ func (e *businessEngine) styleText(args []platformValue) (any, error) {
} }
// formatText 批量修改选区格式,未指定的属性保持各自原值 // formatText 批量修改选区格式,未指定的属性保持各自原值
// 入参: args 页面索引、完整路径数组和格式JSON // 入参: args 页面索引、完整路径数组、格式JSON及可选显式重排标志
// 返回: any 文档信息, error 校验或提交错误 // 返回: any 文档信息, error 校验或提交错误
func (e *businessEngine) formatText(args []platformValue) (any, error) { func (e *businessEngine) formatText(args []platformValue) (any, error) {
return e.changeObjects(func() error { return e.changeObjects(func() error {
@@ -4948,10 +4948,33 @@ func (e *businessEngine) formatText(args []platformValue) (any, error) {
} }
*value = color.Value *value = color.Value
} }
if len(args) > 3 && args[3].Bool() {
return e.currentEditor.ReflowTargetsText(args[0].Int(), paths, patch)
}
return e.currentEditor.FormatTargetsText(args[0].Int(), paths, patch) return e.currentEditor.FormatTargetsText(args[0].Int(), paths, patch)
}) })
} }
// layoutTexts 批量修改段落增量,未指定的选项沿用各对象原值
// 入参: args 页面索引、完整路径数组和段落JSON
// 返回: any 文档信息, error 校验或提交错误
func (e *businessEngine) layoutTexts(args []platformValue) (any, error) {
return e.changeObjects(func() error {
paths, err := e.editorTargetPaths(e.stringsFromJS(args[1]))
if err != nil {
return err
}
var patch struct {
ofdgo.RichTextParagraph
Wrap *bool `json:"wrap"`
}
if err := json.Unmarshal([]byte(args[2].String()), &patch); err != nil {
return err
}
return e.currentEditor.LayoutTargetsText(args[0].Int(), paths, patch.RichTextParagraph, patch.Wrap)
})
}
// editorRichText 将富文本复合图元作为一个文本框提供给编辑器 // editorRichText 将富文本复合图元作为一个文本框提供给编辑器
// 入参: item 浏览器对象, object 标准复合图元, value 富文本, fonts 字体名称, local 局部编辑框, matrix 页面变换 // 入参: item 浏览器对象, object 标准复合图元, value 富文本, fonts 字体名称, local 局部编辑框, matrix 页面变换
func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.GraphicObject, value ofdgo.RichText, fonts map[string]string, local ofdgo.Box, matrix ofdgo.Matrix) { func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.GraphicObject, value ofdgo.RichText, fonts map[string]string, local ofdgo.Box, matrix ofdgo.Matrix) {
@@ -5151,43 +5174,30 @@ func (e *businessEngine) saveRichText(args []platformValue) (any, error) {
} }
return e.currentEditor.ReflowRichText(page, path, value) return e.currentEditor.ReflowRichText(page, path, value)
} }
if len(args) > 5 && args[5].String() != "" {
key := args[5].String()
path, err := e.compositePath(key)
if err != nil {
return err
}
if id != "" { if id != "" {
indexes, err := e.compositeIndexes(key, []string{id}) path, err := e.compositePath(id)
if err != nil { if err != nil {
return err return err
} }
width, offset := 0.0, 0.0 offset, width := 0.0, 0.0
if !box.IsNull() && !box.IsUndefined() && box.Get("frameWidth").Type() == platformTypeNumber { if !box.IsNull() && !box.IsUndefined() && box.Get("frameWidth").Type() == platformTypeNumber {
width = box.Get("frameWidth").Float() width = box.Get("frameWidth").Float()
if box.Get("frameOffset").Type() == platformTypeNumber { if box.Get("frameOffset").Type() == platformTypeNumber {
offset = box.Get("frameOffset").Float() offset = box.Get("frameOffset").Float()
} }
} }
return e.currentEditor.UpdateCompositeRichText(page, path, indexes[0], value, offset, width) return e.currentEditor.UpdateTargetRichText(page, path, value, offset, width)
}
if len(args) > 5 && args[5].String() != "" {
key := args[5].String()
path, err := e.compositePath(key)
if err != nil {
return err
} }
index, err := e.currentEditor.AddCompositeRichText(page, path, ofdgo.Box{X: box.Get("x").Float(), Y: box.Get("y").Float(), W: box.Get("width").Float(), H: box.Get("height").Float()}, value) index, err := e.currentEditor.AddCompositeRichText(page, path, ofdgo.Box{X: box.Get("x").Float(), Y: box.Get("y").Float(), W: box.Get("width").Float(), H: box.Get("height").Float()}, value)
id = fmt.Sprintf("%s/%d", key, index) id = fmt.Sprintf("%s/%d", key, index)
return err return err
} }
if id != "" {
if err := e.currentEditor.UpdateRichText(page, id, value); err != nil {
return err
}
if !box.IsNull() && !box.IsUndefined() && box.Get("frameWidth").Type() == platformTypeNumber {
offset := 0.0
if box.Get("frameOffset").Type() == platformTypeNumber {
offset = box.Get("frameOffset").Float()
}
return e.currentEditor.ResizeRichTextFrame(page, id, offset, box.Get("frameWidth").Float())
}
return nil
}
id, err = e.currentEditor.AddRichText(page, ofdgo.Box{X: box.Get("x").Float(), Y: box.Get("y").Float(), W: box.Get("width").Float(), H: box.Get("height").Float()}, value) id, err = e.currentEditor.AddRichText(page, ofdgo.Box{X: box.Get("x").Float(), Y: box.Get("y").Float(), W: box.Get("width").Float(), H: box.Get("height").Float()}, value)
return err return err
}) })
@@ -8054,6 +8064,7 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
"ofdgoReshapeLine": e.reshapeLine, "ofdgoReshapeLine": e.reshapeLine,
"ofdgoResetImageCrop": e.resetImageCrop, "ofdgoResetImageCrop": e.resetImageCrop,
"ofdgoLayoutText": e.layoutText, "ofdgoLayoutText": e.layoutText,
"ofdgoLayoutTexts": e.layoutTexts,
"ofdgoTransformObjects": e.transformObjects, "ofdgoTransformObjects": e.transformObjects,
"ofdgoCompositeObjects": e.compositeObjects, "ofdgoCompositeObjects": e.compositeObjects,
"ofdgoChangeCompositeObjects": e.changeCompositeObjects, "ofdgoChangeCompositeObjects": e.changeCompositeObjects,
+22 -4
View File
@@ -105,6 +105,24 @@ func (e *Editor) UpdateRichText(page int, id string, value RichText) error {
return e.updateRichTextFrame(page, id, value, 0, 0, false) return e.updateRichTextFrame(page, id, value, 0, 0, false)
} }
// UpdateTargetRichText 一次提交正文或组合内富文本的内容和局部宽度,保留页面变换
// 入参: page 页面索引, path 完整对象路径, value 富文本, offset 局部偏移, width 宽度,零值保留宽度模式
// 返回: error 路径、尺寸或排版错误
func (e *Editor) UpdateTargetRichText(page int, path ObjectPath, value RichText, offset, width float64) error {
if !finite(offset) || !finite(width) || width < 0 || width == 0 && offset != 0 {
return fmt.Errorf("invalid text frame")
}
if len(path.Children) == 0 {
if path.Annotation != "" {
return ErrNotRichText
}
return e.updateRichTextFrame(page, path.ID, value, offset, width, false)
}
index := path.Children[len(path.Children)-1]
path.Children = path.Children[:len(path.Children)-1]
return e.updateCompositeRichText(page, path, index, value, offset, width, false)
}
// ResizeRichTextFrame 调整局部左边界与段落宽度并原子重排,保留页面方向和缩放 // ResizeRichTextFrame 调整局部左边界与段落宽度并原子重排,保留页面方向和缩放
// 入参: page 页面索引, id 对象标识, offset 局部横向偏移, width 新的局部段落宽度 // 入参: page 页面索引, id 对象标识, offset 局部横向偏移, width 新的局部段落宽度
// 返回: error 尺寸、排版或提交错误 // 返回: error 尺寸、排版或提交错误
@@ -1066,17 +1084,17 @@ func richTextMetadataFontSafe(value string) bool {
} }
// formatCompositeText 修改内部富文本格式,普通文字安全提升后保留原实例变换 // formatCompositeText 修改内部富文本格式,普通文字安全提升后保留原实例变换
// 入参: page 页面索引, path 父路径, index 成员序号, patch 格式增量 // 入参: page 页面索引, path 父路径, index 成员序号, patch 格式增量, reflow 显式重排
// 返回: error 识别、排版或提交错误 // 返回: error 识别、排版或提交错误
func (e *Editor) formatCompositeText(page int, path ObjectPath, index int, patch TextFormatPatch) error { func (e *Editor) formatCompositeText(page int, path ObjectPath, index int, patch TextFormatPatch, reflow bool) error {
value, err := e.TargetRichText(page, ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: append(slices.Clone(path.Children), index)}) value, _, _, err := e.PrepareRichText(page, ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: append(slices.Clone(path.Children), index)}, reflow)
if err != nil { if err != nil {
return err return err
} }
if err := value.ApplyFormat(0, value.Len(), patch); err != nil { if err := value.ApplyFormat(0, value.Len(), patch); err != nil {
return err return err
} }
return e.UpdateCompositeRichText(page, path, index, value, 0, 0) return e.updateCompositeRichText(page, path, index, value, 0, 0, reflow)
} }
// updateCompositePlainRichText 将可验证布局的内部文字提升为富文本并保留实例坐标 // updateCompositePlainRichText 将可验证布局的内部文字提升为富文本并保留实例坐标
+58 -3
View File
@@ -886,6 +886,61 @@ func (e *Editor) FormatTargetsText(page int, paths []ObjectPath, patch TextForma
return e.formatTargetsText(page, paths, patch, false) return e.formatTargetsText(page, paths, patch, false)
} }
// ReflowTargetsText 显式重排跨容器文字并应用格式增量,保留对象位置及组合结构
// 入参: page 页面索引, paths 完整文字路径, patch 格式增量
// 返回: error 路径、格式或排版错误
func (e *Editor) ReflowTargetsText(page int, paths []ObjectPath, patch TextFormatPatch) error {
return e.formatTargetsText(page, paths, patch, true)
}
// LayoutTargetsText 原子修改跨容器文字的段落增量,保留各自宽度、变换及未指定的样式
// 定位文字显式重排为富文本,任一对象失败时回滚全部修改及新增资源
// 入参: page 页面索引, paths 完整文字路径, patch 段落增量, wrap 折行选项,nil保持原值
// 返回: error 路径、段落或排版错误
func (e *Editor) LayoutTargetsText(page int, paths []ObjectPath, patch RichTextParagraph, wrap *bool) error {
if patch == (RichTextParagraph{}) && wrap == nil {
return nil
}
apply := func(path ObjectPath) error {
value, _, _, err := e.PrepareRichText(page, path, true)
if err != nil {
return err
}
if wrap != nil {
value.Layout.Wrap = *wrap
}
if err := value.FormatParagraphs(0, value.Len(), patch); err != nil {
return err
}
if strings.HasSuffix(value.Text(), "\n") {
if err := value.FormatParagraphs(value.Len(), value.Len(), patch); err != nil {
return err
}
}
return e.ReflowRichText(page, path, value)
}
return e.editTargets(paths, func(ids, annotations []string) error {
if len(annotations) != 0 {
return ErrNotRichText
}
for _, id := range ids {
if err := apply(ObjectPath{ID: id}); err != nil {
return err
}
}
return nil
}, func(path ObjectPath, indexes []int) error {
for _, index := range indexes {
target := path
target.Children = append(slices.Clone(path.Children), index)
if err := apply(target); err != nil {
return err
}
}
return nil
})
}
// formatTargetsText 统一文字选区格式事务,普通字体和颜色修改沿用定位保留路径 // formatTargetsText 统一文字选区格式事务,普通字体和颜色修改沿用定位保留路径
// 入参: page 页面索引, paths 完整路径, patch 格式增量, reflow 显式重排 // 入参: page 页面索引, paths 完整路径, patch 格式增量, reflow 显式重排
// 返回: error 校验或编辑错误 // 返回: error 校验或编辑错误
@@ -925,14 +980,14 @@ func (e *Editor) formatTargetsText(page int, paths []ObjectPath, patch TextForma
plain = append(plain, id) plain = append(plain, id)
continue continue
} }
value, err := e.richTextObject(page, object) value, _, _, err := e.PrepareRichText(page, ObjectPath{ID: id}, reflow)
if err != nil { if err != nil {
return err return err
} }
if err := value.ApplyFormat(0, value.Len(), patch); err != nil { if err := value.ApplyFormat(0, value.Len(), patch); err != nil {
return err return err
} }
if err := e.UpdateRichText(page, id, value); err != nil { if err := e.updateRichTextFrame(page, id, value, 0, 0, reflow); err != nil {
return err return err
} }
} }
@@ -951,7 +1006,7 @@ func (e *Editor) formatTargetsText(page int, paths []ObjectPath, patch TextForma
plain = append(plain, indexes[i]) plain = append(plain, indexes[i])
continue continue
} }
if err := e.formatCompositeText(page, path, indexes[i], patch); err != nil { if err := e.formatCompositeText(page, path, indexes[i], patch, reflow); err != nil {
return err return err
} }
} }