mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
e35533859c
commit
89a7ef04c7
23 files changed
+410
-114
No files matched your search
+114
-48
@@ -1,4 +1,4 @@
|
||||
import { SVG_NAMESPACE, CanvasEditor, canEditObject, editTextValue, richTextModel, richTextAlignment, fontFaceStyle, missingGlyphMessage, objectEditReason, selectedText, lineShape, selectionBounds, pagePoint } from "./ofdgo_edit.js";
|
||||
import { SVG_NAMESPACE, CanvasEditor, canEditObject, editTextValue, richTextModel, richTextAlignment, richTextLayout, fontFaceStyle, missingGlyphMessage, objectEditReason, selectedText, lineShape, selectionBounds, pagePoint } from "./ofdgo_edit.js";
|
||||
import { FontManager, FontPicker } from "./ofdgo_font.js";
|
||||
import { nativeHost, createEngineWorker, engineLimits, documentBytes, saveFilePicker, directoryPicker } from "./ofdgo_host.js";
|
||||
import { PrintDialog } from "./ofdgo_print.js";
|
||||
@@ -704,8 +704,10 @@ const annotationFields = Object.fromEntries(["CreateForm", "CreateCancel", "Cont
|
||||
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
||||
|
||||
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
textControls: [el.textAlign, el.textFont, el.textFontToggle, el.textFontAdd, el.textFontList, el.textSize, el.textColor, textBackground, ...textFormatButtons, el.undoButton, el.redoButton, el.saveButton, el.saveAsButton, el.fontInput, document.querySelector("#textReflow")],
|
||||
keepTextFocus: () => fontPicker.open || el.closePanel.open || canvasEditor.input?.fontDialog,
|
||||
textControls: [el.textAlign, el.textFont, el.textFontToggle, el.textFontAdd, el.textFontList, el.textSize, el.textColor, el.textLineHeight, el.textSpacing, el.textWrap, el.paragraphButton, el.paragraphPanel, textBackground, ...textFormatButtons, el.undoButton, el.redoButton, el.saveButton, el.saveAsButton, el.fontInput, document.querySelector("#textReflow")],
|
||||
keepTextFocus: () => fontPicker.open || el.closePanel.open || el.paragraphPanel.open || canvasEditor.input?.fontDialog,
|
||||
onTextFormat: changeRangeFormat,
|
||||
onRichInput: ensureRichInput,
|
||||
textClipboardScope: () => `${documentTab().id}:${documentContentKey(state)}:${state.editorInfo?.revision ?? ""}`,
|
||||
getTextClipboard: () => workspace.textClipboard,
|
||||
setTextClipboard: clipboard => { workspace.textClipboard = clipboard; },
|
||||
@@ -828,10 +830,10 @@ el.editorTools.addEventListener("pointerdown", (event) => {
|
||||
if ((canvasEditor.input || canvasEditor.nudge) && event.target.closest("button")) event.preventDefault();
|
||||
});
|
||||
|
||||
function editorClick(button, action) {
|
||||
function editorClick(button, action, keepText = false) {
|
||||
button.addEventListener("click", async () => {
|
||||
if ((canvasEditor.nudge || canvasEditor.nudgeCommit) && !await canvasEditor.commitNudge()) return;
|
||||
if (canvasEditor.input && !await canvasEditor.commitText()) return;
|
||||
if (!keepText && canvasEditor.input && !await canvasEditor.commitText()) return;
|
||||
if (canvasEditor.crop && !await canvasEditor.commitCrop()) return;
|
||||
return action();
|
||||
});
|
||||
@@ -866,16 +868,22 @@ el.textSize.addEventListener("change", () => changeTextStyle(false));
|
||||
el.textColor.addEventListener("change", () => changeTextStyle(true));
|
||||
for (const button of textFormatButtons) button.addEventListener("click", () => changeRangeFormat(button.dataset.textFormat));
|
||||
textBackground.addEventListener("change", () => changeRangeFormat("background", textBackground.value));
|
||||
editorClick(el.textWrap, () => changeParagraph(!currentTextStyle().wrap));
|
||||
editorClick(el.textWrap, () => changeParagraph(!currentTextStyle().wrap), true);
|
||||
el.textAlign.addEventListener("change", () => changeTextAlignment());
|
||||
el.textLineHeight.addEventListener("change", () => changeParagraph());
|
||||
el.textLineHeight.addEventListener("change", () => changeParagraph(undefined, "lineHeight"));
|
||||
editorClick(el.paragraphButton, () => {
|
||||
const item = currentTextStyle();
|
||||
for (const key of ["leftIndent", "rightIndent", "firstLineIndent"]) el[key].value = item[key] || 0;
|
||||
for (const key of ["leftIndent", "rightIndent", "firstLineIndent"]) {
|
||||
const mixed = Boolean(canvasEditor.input?.rich || item.richText) && item[key] === undefined;
|
||||
el[key].required = !mixed;
|
||||
el[key].placeholder = mixed ? "混合" : "";
|
||||
el[key].value = mixed ? "" : item[key] || 0;
|
||||
}
|
||||
el.paragraphStatus.textContent = "";
|
||||
el.paragraphPanel.showModal();
|
||||
});
|
||||
}, true);
|
||||
el.paragraphCancel.addEventListener("click", () => el.paragraphPanel.close());
|
||||
el.paragraphPanel.addEventListener("close", () => canvasEditor.input?.input.focus({ preventScroll: true }));
|
||||
editorClick(el.documentAddButton, () => openCreatePanel(false, true));
|
||||
editorClick(el.documentImportButton, () => openImportPanel(true));
|
||||
editorClick(el.documentManageButton, openDocumentManagement);
|
||||
@@ -1181,20 +1189,26 @@ el.annotationForm.addEventListener("submit", async event => {
|
||||
el.paragraphForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (document.body.hasAttribute("data-busy") || !el.paragraphForm.reportValidity()) return;
|
||||
const item = currentTextStyle();
|
||||
const indents = Object.fromEntries(["leftIndent", "rightIndent", "firstLineIndent"].map(key => [key, Number(el[key].value)]));
|
||||
if (item === state.textDefaults) {
|
||||
Object.assign(item, indents);
|
||||
} else {
|
||||
if (!confirmTextReflow(item)) return;
|
||||
const saved = item.richText ? await saveRichText(item, { ...item.richText, layout: { ...item.richText.layout, ...indents } })
|
||||
: await changeDocument("ofdgoLayoutText", item, null, null, Boolean(item.wrap), item.align || "left",
|
||||
item.paragraphHeight || 0, item.letterSpacing || 0, ...textIndents(indents));
|
||||
if (!saved) return;
|
||||
const item = currentTextStyle(), tab = documentTab();
|
||||
const indents = Object.fromEntries(["leftIndent", "rightIndent", "firstLineIndent"].filter(key => el[key].value !== "").map(key => [key, Number(el[key].value)]));
|
||||
try {
|
||||
if (canvasEditor.input) {
|
||||
if (!await changeInputParagraph(indents)) return;
|
||||
} else if (item === state.textDefaults) {
|
||||
Object.assign(item, indents);
|
||||
} else {
|
||||
if (!confirmTextReflow(item)) return;
|
||||
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 (isForegroundDocument(tab)) el.paragraphPanel.close();
|
||||
} catch (error) {
|
||||
if (isForegroundDocument(tab) && el.paragraphPanel.open) el.paragraphStatus.textContent = error.message;
|
||||
}
|
||||
el.paragraphPanel.close();
|
||||
});
|
||||
el.textSpacing.addEventListener("change", () => changeParagraph());
|
||||
el.textSpacing.addEventListener("change", () => changeParagraph(undefined, "letterSpacing"));
|
||||
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) => {
|
||||
@@ -3957,24 +3971,20 @@ async function editCanvasObject(item, point = null) {
|
||||
return openInsertPanel(item);
|
||||
}
|
||||
const openSeq = state.openSeq;
|
||||
const layoutKnown = canEditObject(item, "layoutKnown");
|
||||
tab.editTask = true;
|
||||
setBusy(true, "", 0, "", tab);
|
||||
try {
|
||||
const data = await callWASM("ofdgoEditorFont", item.font, !layoutKnown);
|
||||
const data = await callWASM("ofdgoEditorFont", item.font, !canEditObject(item, "layoutKnown"));
|
||||
if (!isForegroundDocument(tab) || openSeq !== state.openSeq) return;
|
||||
const face = new FontFace(`ofdgo-edit-${item.font}`, data.bytes, fontFaceStyle(data.bytes));
|
||||
await face.load();
|
||||
if (!isForegroundDocument(tab) || openSeq !== state.openSeq) return;
|
||||
let rich = null;
|
||||
if (item.richText) rich = await loadRichText(item, face, tab, data.bytes);
|
||||
else if (layoutKnown) {
|
||||
try { rich = await loadRichText(item, face, tab, data.bytes); } catch {}
|
||||
}
|
||||
const rich = await loadRichText(item, face, tab, data.bytes);
|
||||
if (!isForegroundDocument(tab) || openSeq !== state.openSeq) return;
|
||||
const source = rich || layoutKnown ? null : await callWASM("ofdgoPreviewText", item.index, item.id, item.text);
|
||||
const source = rich ? null : await callWASM("ofdgoPreviewText", item.index, item.id, item.text);
|
||||
if (isForegroundDocument(tab) && openSeq === state.openSeq && item.node.isConnected) {
|
||||
canvasEditor.editText(item, face, source, rich);
|
||||
canvasEditor.input.fontResource = data.bytes;
|
||||
}
|
||||
} catch (err) {
|
||||
if (isForegroundDocument(tab) && openSeq === state.openSeq) {
|
||||
@@ -4127,10 +4137,14 @@ function openInsertPanel(item = null) {
|
||||
|
||||
function currentTextStyle() {
|
||||
const text = selectedText(canvasEditor.selected) || state.textDefaults;
|
||||
if (canvasEditor.input?.rich) return { ...text, ...canvasEditor.input.rich.state() };
|
||||
if (canvasEditor.input?.rich) {
|
||||
const rich = canvasEditor.input.rich, layout = richTextLayout(rich.model, ...rich.selection());
|
||||
return { ...text, ...rich.state(), ...layout, paragraphHeight: layout.lineHeight };
|
||||
}
|
||||
if (text.richText) {
|
||||
const formats = text.richText.runs.map(run => run.format);
|
||||
return { ...text, ...Object.fromEntries(["font", "size", "color", "bold", "italic", "underline", "strikeout", "shadow", "background"]
|
||||
const layout = richTextLayout(text.richText);
|
||||
return { ...text, ...layout, paragraphHeight: layout.lineHeight, ...Object.fromEntries(["font", "size", "color", "bold", "italic", "underline", "strikeout", "shadow", "background"]
|
||||
.map(key => [key, formats.every(format => format[key] === formats[0]?.[key]) ? formats[0]?.[key] : undefined])) };
|
||||
}
|
||||
return canvasEditor.input?.style ? { ...text, ...canvasEditor.input.style } : text;
|
||||
@@ -4143,6 +4157,7 @@ async function loadRichText(item, firstFace, tab = documentTab(), firstData = nu
|
||||
return callTabWASM(tab, name, ...args);
|
||||
};
|
||||
const prepared = item.richText ? { model: item.richText, frame: item.textFrame } : await callWASM("ofdgoGetRichText", item.index, item.id, reflow);
|
||||
if (prepared.unsupported) return null;
|
||||
const model = prepared.model;
|
||||
const remaining = firstData ? { ...model, runs: model.runs.filter(run => run.format.font !== item.font) } : model;
|
||||
const fontData = remaining.runs.length ? await callWASM("ofdgoRichTextFonts", JSON.stringify(remaining)) : [];
|
||||
@@ -4170,8 +4185,15 @@ async function ensureRichInput(reflow = false) {
|
||||
if (editing.item.selectionIDs) return false;
|
||||
if (editing.rich) return true;
|
||||
if (editing.composing || editing.saving) return false;
|
||||
const rich = await loadRichText(editing.item, editing.face, documentTab(), null, reflow);
|
||||
if (canvasEditor.input !== editing) return false;
|
||||
const tab = documentTab();
|
||||
let rich = await loadRichText(editing.item, editing.face, tab, editing.fontResource, reflow);
|
||||
if (canvasEditor.input !== editing || !isForegroundDocument(tab)) return false;
|
||||
if (!rich && !reflow) {
|
||||
if (!confirmTextReflow(editing.item)) return false;
|
||||
rich = await loadRichText(editing.item, editing.face, tab, editing.fontResource, true);
|
||||
}
|
||||
if (canvasEditor.input !== editing || !isForegroundDocument(tab)) return false;
|
||||
if (!rich) throw new Error("文字暂不可重排");
|
||||
return Boolean(editing.rich) || canvasEditor.promoteText(rich);
|
||||
}
|
||||
|
||||
@@ -4187,6 +4209,7 @@ async function reflowCanvasText() {
|
||||
|
||||
async function changeRangeFormat(key, value) {
|
||||
if (!state.ready || document.body.hasAttribute("data-busy") || canvasEditor.input?.composing) return;
|
||||
const tab = documentTab(), editing = canvasEditor.input;
|
||||
try {
|
||||
if (!canvasEditor.selected && !canvasEditor.input) {
|
||||
state.textDefaults[key] = value ?? (key === "background" ? state.textDefaults.background ? "" : textBackground.value : !state.textDefaults[key]);
|
||||
@@ -4202,7 +4225,9 @@ async function changeRangeFormat(key, value) {
|
||||
if (!await ensureRichInput()) return;
|
||||
const rich = canvasEditor.input.rich, current = rich.state()[key];
|
||||
rich.apply({ [key]: value ?? (key === "background" ? current ? "" : textBackground.value : current !== true) });
|
||||
} catch (error) { showError(error, false); }
|
||||
} catch (error) {
|
||||
if (isForegroundDocument(tab) && (!editing || canvasEditor.input === editing)) showError(error, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateRichText(item, model, width, fonts = []) {
|
||||
@@ -4439,6 +4464,12 @@ async function changeTextFont() {
|
||||
const editing = canvasEditor.input;
|
||||
if (editing) {
|
||||
if (editing.composing) return;
|
||||
try {
|
||||
if (!await ensureRichInput() || canvasEditor.input !== editing || !isForegroundDocument(tab)) return;
|
||||
} catch (error) {
|
||||
if (canvasEditor.input === editing && isForegroundDocument(tab)) showError(error, false);
|
||||
return;
|
||||
}
|
||||
editing.rich?.remember();
|
||||
tab.editTask = true;
|
||||
setBusy(true, "", 0, "", tab);
|
||||
@@ -4566,7 +4597,7 @@ async function changeTextAlignment() {
|
||||
editing.rich.align(align);
|
||||
} else if (canvasEditor.selected?.richText) {
|
||||
const item = canvasEditor.selected;
|
||||
await saveRichText(item, { ...item.richText, paragraphs: undefined, layout: { ...item.richText.layout, align } });
|
||||
await saveRichParagraphs(item, { align });
|
||||
} else await changeParagraph();
|
||||
} catch (error) {
|
||||
if (isForegroundDocument(tab) && canvasEditor.input === editing && (!editing || editing.alignSequence === sequence)) showError(error, false);
|
||||
@@ -4575,8 +4606,23 @@ async function changeTextAlignment() {
|
||||
}
|
||||
}
|
||||
|
||||
async function changeParagraph(wrap) {
|
||||
const item = currentTextStyle();
|
||||
async function saveRichParagraphs(item, patch) {
|
||||
const { wrap, ...values } = patch, model = item.richText;
|
||||
return saveRichText(item, { ...model, layout: { ...model.layout, ...(wrap !== undefined ? { wrap } : {}) },
|
||||
paragraphs: model.runs.map(run => run.text).join("").split("\n").map((_, i) => ({ align: "", ...model.paragraphs?.[i], ...values })) });
|
||||
}
|
||||
|
||||
async function changeParagraph(wrap, changed) {
|
||||
const item = currentTextStyle(), tab = documentTab();
|
||||
if (canvasEditor.input) {
|
||||
const patch = wrap !== undefined ? { wrap: Boolean(wrap) } : {};
|
||||
for (const [input, key, original] of [[el.textLineHeight, "lineHeight", item.paragraphHeight], [el.textSpacing, "letterSpacing", item.letterSpacing]]) {
|
||||
if (key !== changed) continue;
|
||||
if (!input.checkValidity()) return updateObjectControls(canvasEditor.selected, true);
|
||||
if (input.value !== "" || original !== undefined) patch[key] = inputMillimeters(input, original || 0);
|
||||
}
|
||||
return changeInputParagraph(patch);
|
||||
}
|
||||
wrap ??= item.wrap;
|
||||
if (document.body.hasAttribute("data-busy") || item.draft) return;
|
||||
if (!el.textLineHeight.checkValidity()) {
|
||||
@@ -4598,9 +4644,26 @@ async function changeParagraph(wrap) {
|
||||
updateObjectControls(item, true);
|
||||
return;
|
||||
}
|
||||
if (item.richText) await saveRichText(item, { ...item.richText, layout: { ...item.richText.layout, wrap: Boolean(wrap), lineHeight: height, letterSpacing: spacing } });
|
||||
else await changeDocument("ofdgoLayoutText", item, null, null, Boolean(wrap), el.textAlign.value, height, spacing, ...textIndents(item));
|
||||
updateObjectControls(canvasEditor.selected, true);
|
||||
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);
|
||||
}
|
||||
|
||||
async function changeInputParagraph(patch) {
|
||||
const editing = canvasEditor.input, tab = documentTab();
|
||||
if (!editing || editing.composing || editing.saving || document.body.hasAttribute("data-busy")) return false;
|
||||
try {
|
||||
if (!await ensureRichInput() || canvasEditor.input !== editing || !isForegroundDocument(tab)) return false;
|
||||
editing.rich.paragraph(patch);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (canvasEditor.input === editing && isForegroundDocument(tab)) showError(error, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function changeTextStyle(color) {
|
||||
@@ -4616,11 +4679,14 @@ async function changeTextStyle(color) {
|
||||
const size = color ? item.size : inputMillimeters(el.textSize, item.size);
|
||||
const selectedColor = el.textColor.value;
|
||||
const fill = color && selectedColor !== item.color ? selectedColor : null;
|
||||
if (item.richText && !canvasEditor.input) {
|
||||
try { if (!await ensureRichInput()) return; } catch (error) { showError(error, false); return; }
|
||||
}
|
||||
if (canvasEditor.input) {
|
||||
if (!color && canvasEditor.input.source && !canvasEditor.input.reflow && size !== item.size && !confirmTextReflow(item)) return;
|
||||
if (canvasEditor.input || item.richText) {
|
||||
const editing = canvasEditor.input, tab = documentTab();
|
||||
try {
|
||||
if (!await ensureRichInput() || !isForegroundDocument(tab) || editing && canvasEditor.input !== editing) return;
|
||||
} catch (error) {
|
||||
if (isForegroundDocument(tab) && (!editing || canvasEditor.input === editing)) showError(error, false);
|
||||
return;
|
||||
}
|
||||
canvasEditor.setTextStyle(color ? { color: selectedColor } : { size });
|
||||
updateObjectControls(canvasEditor.selected, true);
|
||||
return;
|
||||
@@ -12487,7 +12553,7 @@ function updateObjectControls(item, reset = false) {
|
||||
el.undoButton.disabled = !state.editorInfo?.canUndo || !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 || item?.draft) || Boolean(canvasEditor.input);
|
||||
el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items);
|
||||
el.textWrap.disabled = el.textLineHeight.disabled || text === state.textDefaults;
|
||||
const reflow = document.querySelector("#textReflow"), positioned = canvasEditor.input?.source && (item?.selectionIDs || !canEditObject(item, "layoutKnown"));
|
||||
reflow.hidden = !positioned;
|
||||
@@ -12500,8 +12566,8 @@ function updateObjectControls(item, reset = false) {
|
||||
const alignment = 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.value = alignment;
|
||||
el.textLineHeight.placeholder = layoutKnown ? "自动" : "原文";
|
||||
el.textSpacing.placeholder = layoutKnown ? "0" : "原文";
|
||||
el.textLineHeight.placeholder = (rich || item?.richText) && text.paragraphHeight === undefined ? "混合" : layoutKnown ? "自动" : "原文";
|
||||
el.textSpacing.placeholder = (rich || item?.richText) && text.letterSpacing === undefined ? "混合" : layoutKnown ? "0" : "原文";
|
||||
el.textWrap.setAttribute("data-pressed", String(Boolean(text.wrap)));
|
||||
if (reset || document.activeElement !== el.textLineHeight) {
|
||||
el.textLineHeight.value = text.paragraphHeight ? String(displayPoints(text.paragraphHeight)) : "";
|
||||
|
||||
+72
-12
@@ -59,6 +59,17 @@ export function richTextAlignment(model, start = 0, end = model.runs.reduce((n,
|
||||
return align;
|
||||
}
|
||||
|
||||
export function richTextLayout(model, start = 0, end = model.runs.reduce((n, run) => n + run.text.length, 0)) {
|
||||
const [first, last] = paragraphRange(model.runs.map(run => run.text).join(""), start, end);
|
||||
const value = { ...model.layout, ...model.paragraphs?.[first] };
|
||||
for (const key of ["lineHeight", "letterSpacing", "leftIndent", "rightIndent", "firstLineIndent"]) {
|
||||
value[key] ??= 0;
|
||||
for (let i = first + 1; i <= last; i++) if ((model.paragraphs?.[i]?.[key] ?? model.layout[key] ?? 0) !== value[key]) value[key] = undefined;
|
||||
}
|
||||
value.align = richTextAlignment(model, start, end);
|
||||
return value;
|
||||
}
|
||||
|
||||
function replaceParagraphs(model, text, start, end, inserted, paragraphs) {
|
||||
if (!model.paragraphs?.length && !paragraphs?.length) return;
|
||||
const first = text.slice(0, start).split("\n").length - 1, last = text.slice(0, end).split("\n").length - 1;
|
||||
@@ -179,6 +190,7 @@ export class RichTextSession {
|
||||
this.model.runs = normalizeRuns(model.runs);
|
||||
this.initial = JSON.stringify(this.model);
|
||||
this.recordedParagraphs = this.model.paragraphs;
|
||||
this.recordedLayout = this.model.layout;
|
||||
this.faces = faces;
|
||||
for (const face of faces.values()) document.fonts.add(face);
|
||||
this.imports = new Map(imports.map(font => [font.key, font.data]));
|
||||
@@ -232,13 +244,16 @@ export class RichTextSession {
|
||||
const change = textChange(before, this.model.runs);
|
||||
const entry = { change, typing: { ...this.typing }, caret: [...this.caret] }, old = this.entries[this.index];
|
||||
const paragraphsChanged = JSON.stringify(this.recordedParagraphs) !== JSON.stringify(this.model.paragraphs);
|
||||
const layoutChanged = JSON.stringify(this.recordedLayout) !== JSON.stringify(this.model.layout);
|
||||
if (layoutChanged) entry.layout = { before: this.recordedLayout, after: this.model.layout };
|
||||
this.recordedLayout = this.model.layout;
|
||||
if (paragraphsChanged) entry.paragraphs = { before: this.recordedParagraphs, after: this.model.paragraphs };
|
||||
this.recordedParagraphs = this.model.paragraphs;
|
||||
const changed = change.before.length || change.after.length;
|
||||
if (!changed && !paragraphsChanged && JSON.stringify(entry.typing) === JSON.stringify(old.typing)) return;
|
||||
if (!changed && !paragraphsChanged && !layoutChanged && JSON.stringify(entry.typing) === JSON.stringify(old.typing)) return;
|
||||
if (!changed) entry.change = null;
|
||||
this.entries.splice(++this.index, Infinity, entry);
|
||||
if (changed || paragraphsChanged) this.preview();
|
||||
if (changed || paragraphsChanged || layoutChanged) this.preview();
|
||||
}
|
||||
undo(direction) {
|
||||
if (this.editing.composing || this.editing.saving) return;
|
||||
@@ -252,6 +267,9 @@ export class RichTextSession {
|
||||
}
|
||||
const paragraphs = (direction < 0 ? this.entries[this.index] : entry).paragraphs;
|
||||
if (paragraphs) this.model.paragraphs = direction < 0 ? paragraphs.before : paragraphs.after;
|
||||
const layout = (direction < 0 ? this.entries[this.index] : entry).layout;
|
||||
if (layout) this.model.layout = direction < 0 ? layout.before : layout.after;
|
||||
this.recordedLayout = this.model.layout;
|
||||
this.recordedParagraphs = this.model.paragraphs;
|
||||
this.index += direction;
|
||||
this.typing = { ...entry.typing };
|
||||
@@ -296,10 +314,15 @@ export class RichTextSession {
|
||||
this.editor.options.onTextChange();
|
||||
}
|
||||
align(value) {
|
||||
this.paragraph({ align: value });
|
||||
}
|
||||
paragraph(values) {
|
||||
if (this.editing.composing || this.editing.saving) return;
|
||||
this.remember();
|
||||
const [start, end] = this.selection(), [first, last] = paragraphRange(this.text, start, end);
|
||||
this.model.paragraphs = this.text.split("\n").map((_, i) => ({ align: i >= first && i <= last ? value : this.model.paragraphs?.[i]?.align || "" }));
|
||||
const { wrap, ...patch } = values;
|
||||
if (wrap !== undefined) this.model.layout = { ...this.model.layout, wrap };
|
||||
if (Object.keys(patch).length) this.model.paragraphs = this.text.split("\n").map((_, i) => ({ align: "", ...this.model.paragraphs?.[i], ...(i >= first && i <= last ? patch : {}) }));
|
||||
this.record(this.model.runs);
|
||||
this.render();
|
||||
this.editor.options.onTextChange();
|
||||
@@ -350,7 +373,10 @@ export class RichTextSession {
|
||||
const keys = new Set(runs.map(run => run.format.font));
|
||||
const fonts = new Map([...keys].map(key => [key, this.imports.get(key) || this.resources.get(key)]).filter(([, data]) => data));
|
||||
const first = this.text.slice(0, start).split("\n").length - 1;
|
||||
const paragraphs = text.split("\n").map((_, i) => ({ align: this.model.paragraphs?.[first + i]?.align || this.model.layout.align || "left" }));
|
||||
const paragraphs = text.split("\n").map((_, i) => {
|
||||
const layout = { ...this.model.layout, ...this.model.paragraphs?.[first + i] };
|
||||
return Object.fromEntries(["align", "lineHeight", "letterSpacing", "leftIndent", "rightIndent", "firstLineIndent"].map(key => [key, key === "align" ? layout[key] || "left" : layout[key] || 0]));
|
||||
});
|
||||
const clipboard = { token: crypto.randomUUID(), runs, text, paragraphs, scope: this.editor.options.textClipboardScope?.(),
|
||||
fonts, faces: new Map([...this.faces].filter(([key]) => keys.has(key))), choices: new Map([...this.choices].filter(([key]) => keys.has(key))) };
|
||||
if (this.editor.options.setTextClipboard) this.editor.options.setTextClipboard(clipboard);
|
||||
@@ -411,7 +437,8 @@ export class RichTextSession {
|
||||
const previous = this.renderedLines || [];
|
||||
let offset = 0;
|
||||
this.renderedLines = lines.map((parts, index) => {
|
||||
const align = this.model.paragraphs?.[index]?.align || this.model.layout.align || "left";
|
||||
const layout = { ...this.model.layout, ...this.model.paragraphs?.[index] };
|
||||
const align = layout.align || this.model.layout.align || "left";
|
||||
const distribute = align === "distributed" && !CSS.supports("text-justify", "inter-character");
|
||||
const key = JSON.stringify([parts.map(part => [part.text, part.format, this.faces.get(part.format.font)?.family]), align, layout, distribute ? input.offsetWidth : null]);
|
||||
let paragraph = (!autoWidth || !distribute) && previous[index]?.key === key && previous[index].node.parentNode === input ? previous[index].node : null;
|
||||
@@ -431,6 +458,8 @@ export class RichTextSession {
|
||||
paragraph.append(document.createElement("br"));
|
||||
}
|
||||
Object.assign(paragraph.style, { minWidth: layout.wrap ? "" : "max-content", textAlign: distribute ? "left" : align === "distributed" ? "justify" : align,
|
||||
marginLeft: `${(layout.leftIndent || 0) * PX_PER_MM}px`, marginRight: `${(layout.rightIndent || 0) * PX_PER_MM}px`,
|
||||
lineHeight: layout.lineHeight ? `${layout.lineHeight * PX_PER_MM}px` : "normal", letterSpacing: `${(layout.letterSpacing || 0) * PX_PER_MM}px`, textIndent: `${(layout.firstLineIndent || 0) * PX_PER_MM}px`,
|
||||
textAlignLast: !distribute && align === "distributed" ? "justify" : "auto", textJustify: align === "distributed" ? "inter-character" : "auto" });
|
||||
}
|
||||
if (distribute && (autoWidth || previous[index]?.node !== paragraph)) distributed.push(paragraph);
|
||||
@@ -1783,7 +1812,7 @@ export class CanvasEditor {
|
||||
else paragraph.append(document.createElement("br"));
|
||||
input.append(paragraph);
|
||||
}
|
||||
const left = rich?.model.layout.leftIndent || item.leftIndent || 0;
|
||||
const left = rich ? 0 : item.leftIndent || 0;
|
||||
const frame = { ...(rich?.frame || item.textFrame || { width: item.width, height: item.height, matrix: [1, 0, 0, 1, item.x, item.y] }) };
|
||||
if (!rich?.frame && item.paragraphWidth != null) frame.width = item.paragraphWidth;
|
||||
const [a, b, c, d, e, f] = frame.matrix;
|
||||
@@ -1844,6 +1873,16 @@ export class CanvasEditor {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const copy = this.options.getTextClipboard ? this.options.getTextClipboard() : this.richClipboard;
|
||||
if (copy && event.clipboardData.getData(RICH_CLIPBOARD) === copy.token && event.clipboardData.getData("text/plain") === copy.text && this.options.onRichInput) {
|
||||
const data = new Map([RICH_CLIPBOARD, "text/plain"].map(key => [key, event.clipboardData.getData(key)]));
|
||||
const value = editTextValue(input), caret = editTextSelection(input, editing.range);
|
||||
Promise.resolve(this.options.onRichInput()).then(ready => {
|
||||
if (ready && this.input === editing && editing.rich.text === value && (!caret || editing.rich.selection().every((at, i) => at === caret[i])))
|
||||
return editing.rich.paste({ preventDefault() {}, clipboardData: { getData: key => data.get(key) || "" } });
|
||||
}).catch(error => { if (this.input === editing) this.setTextError(error); });
|
||||
return;
|
||||
}
|
||||
document.execCommand("insertText", false, event.clipboardData.getData("text/plain"));
|
||||
});
|
||||
input.addEventListener("beforeinput", event => {
|
||||
@@ -1901,10 +1940,11 @@ export class CanvasEditor {
|
||||
if (event.isComposing || editing.composing || editing.saving) {
|
||||
return;
|
||||
}
|
||||
if (editing.rich && (event.ctrlKey || event.metaKey) && !event.altKey && ["b", "i", "u"].includes(event.key.toLowerCase())) {
|
||||
if ((editing.rich || this.options.onTextFormat) && (event.ctrlKey || event.metaKey) && !event.altKey && ["b", "i", "u"].includes(event.key.toLowerCase())) {
|
||||
event.preventDefault();
|
||||
const key = { b: "bold", i: "italic", u: "underline" }[event.key.toLowerCase()];
|
||||
editing.rich.apply({ [key]: editing.rich.state()[key] !== true });
|
||||
if (this.options.onTextFormat) this.options.onTextFormat(key);
|
||||
else editing.rich.apply({ [key]: editing.rich.state()[key] !== true });
|
||||
} else if ((editing.source || editing.rich) && (event.ctrlKey || event.metaKey) && !event.altKey && ["z", "y"].includes(event.key.toLowerCase())) {
|
||||
event.preventDefault();
|
||||
this.textUndo(event.key.toLowerCase() === "y" || event.shiftKey ? 1 : -1);
|
||||
@@ -1917,8 +1957,8 @@ export class CanvasEditor {
|
||||
this.commitText();
|
||||
}
|
||||
});
|
||||
input.addEventListener("copy", event => editing.rich?.copy(event));
|
||||
input.addEventListener("cut", event => editing.rich?.copy(event, true));
|
||||
input.addEventListener("copy", event => this.copyText(event));
|
||||
input.addEventListener("cut", event => this.copyText(event, true));
|
||||
if (editing.rich) editing.rich.render();
|
||||
input.focus({ preventScroll: true });
|
||||
if (!item.draft) document.getSelection().selectAllChildren(input);
|
||||
@@ -1931,13 +1971,33 @@ export class CanvasEditor {
|
||||
if (this.input?.rich) this.input.rich.undo(direction);
|
||||
else this.restoreSourceText(direction);
|
||||
}
|
||||
copyText(event, cut = false) {
|
||||
const editing = this.input;
|
||||
if (!editing || editing.composing || editing.saving) return;
|
||||
if (editing.rich) return editing.rich.copy(event, cut);
|
||||
if (editing.item.selectionIDs) return;
|
||||
const range = editTextSelection(editing.input, editing.range);
|
||||
if (!range || range[0] === range[1]) return;
|
||||
const [start, end] = range.sort((a, b) => a - b), text = editTextValue(editing.input).slice(start, end);
|
||||
const model = richTextModel({ ...editing.item, ...editing.style, text });
|
||||
const font = editing.fontData || editing.fontResource;
|
||||
const clipboard = { token: crypto.randomUUID(), text, runs: model.runs, scope: this.options.textClipboardScope?.(),
|
||||
fonts: new Map(font ? [[editing.item.font, font]] : []), faces: new Map([[editing.item.font, editing.face]]), choices: new Map(),
|
||||
paragraphs: text.split("\n").map(() => ({ align: "left" })) };
|
||||
if (this.options.setTextClipboard) this.options.setTextClipboard(clipboard);
|
||||
else this.richClipboard = clipboard;
|
||||
event.preventDefault();
|
||||
event.clipboardData.setData("text/plain", text);
|
||||
event.clipboardData.setData(RICH_CLIPBOARD, clipboard.token);
|
||||
if (cut) document.execCommand("delete");
|
||||
}
|
||||
|
||||
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 width = Math.max(0, frame.width - (rich ? 0 : layout.leftIndent || 0) - (rich ? 0 : layout.rightIndent || 0)) * PX_PER_MM + 4;
|
||||
Object.assign(input.style, { width: rich?.model.autoWidth && !layout.wrap ? "max-content" : `${width}px`, minWidth: "4px", minHeight: "0px", height: "auto" });
|
||||
}
|
||||
|
||||
@@ -1992,7 +2052,7 @@ export class CanvasEditor {
|
||||
document.removeEventListener("selectionchange", editing.selectionListener);
|
||||
editing.history = null;
|
||||
editing.range = null;
|
||||
const [a, b, c, d, e, f] = editing.frame.matrix, left = rich.model.layout.leftIndent || 0;
|
||||
const [a, b, c, d, e, f] = editing.frame.matrix, left = 0;
|
||||
editing.input.style.transform = `matrix(${a},${b},${c},${d},${(e + a * left) * PX_PER_MM},${(f + b * left) * PX_PER_MM})`;
|
||||
session.render();
|
||||
session.preview();
|
||||
|
||||
Reference in new issue
Block a user