|
|
|
@@ -110,6 +110,7 @@ function createDocumentState() {
|
|
|
|
|
styleOriginal: null,
|
|
|
|
|
objectClipboardToken: null,
|
|
|
|
|
styleClipboard: null,
|
|
|
|
|
objectDistribution: null,
|
|
|
|
|
outlineExpanded: new Map(),
|
|
|
|
|
textFonts: [],
|
|
|
|
|
textFontID: null,
|
|
|
|
@@ -683,9 +684,12 @@ function createPrintDialog(tab) {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openPrintPanel() {
|
|
|
|
|
const tab = documentTab();
|
|
|
|
|
async function openPrintPanel() {
|
|
|
|
|
const tab = documentTab(), content = documentContentKey(state);
|
|
|
|
|
if (!tab.state.doc || !tab.state.ready || document.body.hasAttribute("data-busy") || formDialogOpen()) return;
|
|
|
|
|
const current = () => isForegroundDocument(tab) && content === documentContentKey(state);
|
|
|
|
|
if (!await canvasEditor.commitNudge() || !current() || !await canvasEditor.commitText() || !current()
|
|
|
|
|
|| !await canvasEditor.commitCrop() || !current() || document.body.hasAttribute("data-busy") || formDialogOpen()) return;
|
|
|
|
|
tab.print ||= createPrintDialog(tab);
|
|
|
|
|
tab.print.open({tab, document:documentEntryKey(tab.state.doc.documents?.[tab.state.doc.documentIndex || 0]), call:(name, ...args) => callTabWASM(tab, name, ...args), page:tab.state.pageIndex,
|
|
|
|
|
selection:tab.state.pageSelection.size ? selectedPageIndexes() : [], annotations:tab.state.renderAnnotations,
|
|
|
|
@@ -1260,7 +1264,7 @@ el.objectAlign.addEventListener("change", () => {
|
|
|
|
|
el.objectDistribute.addEventListener("change", () => {
|
|
|
|
|
const axis = el.objectDistribute.value;
|
|
|
|
|
el.objectDistribute.value = "";
|
|
|
|
|
if (axis && canvasEditor.items().length >= 3) {
|
|
|
|
|
if (axis && !el.objectDistribute.disabled && state.objectDistribution?.axes?.includes(axis)) {
|
|
|
|
|
return changeDocument("ofdgoDistributeObjects", canvasEditor.selected, axis);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
@@ -1307,7 +1311,10 @@ for (const [button, tool] of [[el.drawLineButton, "line"], [el.drawRectangleButt
|
|
|
|
|
}
|
|
|
|
|
editorClick(el.eraseButton, () => toggleDrawingTool(el.eraseMode.value));
|
|
|
|
|
el.eraseMode.addEventListener("change", () => {
|
|
|
|
|
if (canvasEditor.tool.startsWith("erase-")) canvasEditor.setTool(el.eraseMode.value);
|
|
|
|
|
if (canvasEditor.tool.startsWith("erase-")) {
|
|
|
|
|
canvasEditor.setTool(el.eraseMode.value);
|
|
|
|
|
canvasEditor.focus();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
for (const input of [el.shapeFill, el.shapeStroke]) {
|
|
|
|
|
input.addEventListener("change", changeShapeStyle);
|
|
|
|
@@ -1407,9 +1414,11 @@ for (const [button, ungroup] of [[el.groupObjectsButton, false], [el.ungroupObje
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
el.arrowTool.addEventListener("change", async () => {
|
|
|
|
|
const tab = documentTab(), content = documentContentKey(state);
|
|
|
|
|
const current = () => isForegroundDocument(tab) && state.editing && content === documentContentKey(state);
|
|
|
|
|
const tool = el.arrowTool.value;
|
|
|
|
|
el.arrowTool.value = "";
|
|
|
|
|
if (await canvasEditor.commitText() && await canvasEditor.commitCrop()) {
|
|
|
|
|
if (await canvasEditor.commitText() && current() && await canvasEditor.commitCrop() && current()) {
|
|
|
|
|
const item = canvasEditor.selected;
|
|
|
|
|
if (lineShape(item?.shape)) {
|
|
|
|
|
const box = item.geometry;
|
|
|
|
@@ -1420,6 +1429,7 @@ el.arrowTool.addEventListener("change", async () => {
|
|
|
|
|
state.selectObjects = true;
|
|
|
|
|
canvasEditor.setTool(tool);
|
|
|
|
|
updateControls();
|
|
|
|
|
canvasEditor.focus();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
el.copyStyleButton.addEventListener("click", async () => {
|
|
|
|
@@ -1807,6 +1817,7 @@ el.annotationButton.addEventListener("click", toggleAnnotations);
|
|
|
|
|
document.addEventListener("copy", copySelection);
|
|
|
|
|
document.addEventListener("cut", copySelection);
|
|
|
|
|
el.viewerPanel.addEventListener("paste", pasteEditorContent);
|
|
|
|
|
el.editorTools.addEventListener("paste", pasteEditorContent);
|
|
|
|
|
document.addEventListener("selectionchange", syncSelection);
|
|
|
|
|
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
|
|
|
|
el.outputCompression.addEventListener("change", () => {
|
|
|
|
@@ -1933,6 +1944,11 @@ el.documentNewTab.addEventListener("click", openEmptyDocumentTab);
|
|
|
|
|
updateSidebarState();
|
|
|
|
|
boot();
|
|
|
|
|
|
|
|
|
|
function editorShortcutTarget(target) {
|
|
|
|
|
const field = target.closest?.("input, textarea, select, [contenteditable]");
|
|
|
|
|
return !field || el.editorTools.contains(field) && field.matches('select, input[type="checkbox"], input[type="radio"], input[type="color"], input[type="range"]');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleKeyDown(event) {
|
|
|
|
|
if (sealPreview) {
|
|
|
|
|
if (event.key === "Escape") { event.preventDefault(); finishSealPosition(); }
|
|
|
|
@@ -1965,10 +1981,11 @@ function handleKeyDown(event) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "a" && state.doc
|
|
|
|
|
&& el.viewerPanel.contains(target) && !target.closest("input, textarea, select, [contenteditable]")) {
|
|
|
|
|
&& (el.viewerPanel.contains(target) || state.editing && el.editorTools.contains(target)) && editorShortcutTarget(target)) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (!document.body.hasAttribute("data-busy") && !formDialogOpen()) {
|
|
|
|
|
selectDocumentText();
|
|
|
|
|
if (state.editing) canvasEditor.selectAll(state.pageIndex);
|
|
|
|
|
else selectDocumentText();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -1994,7 +2011,7 @@ function handleKeyDown(event) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (event.ctrlKey || event.metaKey) {
|
|
|
|
|
if (state.editing && !target.closest("input, textarea, select, [contenteditable]")
|
|
|
|
|
if (state.editing && editorShortcutTarget(target)
|
|
|
|
|
&& (key.toLowerCase() === "z" || (!event.shiftKey && key.toLowerCase() === "y"))) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
(key.toLowerCase() === "y" || event.shiftKey ? el.redoButton : el.undoButton).click();
|
|
|
|
@@ -2010,6 +2027,11 @@ function handleKeyDown(event) {
|
|
|
|
|
if (!state.doc) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (state.editing && !event.shiftKey && el.editorTools.contains(target) && editorShortcutTarget(target)
|
|
|
|
|
&& (key === "Delete" || key === "F2")) {
|
|
|
|
|
canvasEditor.keyDown(event, true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (key === "Escape" && !event.shiftKey && state.documentSelection && el.viewerPanel.contains(target)
|
|
|
|
|
&& !target.closest("input, textarea, select, [contenteditable]")) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
@@ -2019,11 +2041,7 @@ function handleKeyDown(event) {
|
|
|
|
|
}
|
|
|
|
|
if (key === "F3") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (state.searchMatches.length) {
|
|
|
|
|
selectSearchMatch(event.shiftKey ? Math.max(0, state.searchIndex) - 1 : state.searchIndex + 1);
|
|
|
|
|
} else {
|
|
|
|
|
focusSearch();
|
|
|
|
|
}
|
|
|
|
|
focusSearch(event.shiftKey ? -1 : 1);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (key === "Escape" && !event.shiftKey && !el.searchPanel.hidden && (el.pageListPanel.contains(target) || el.viewerPanel.contains(target))) {
|
|
|
|
@@ -4353,15 +4371,15 @@ async function validateRichText(item, model, width, fonts = []) {
|
|
|
|
|
attempted.add(id);
|
|
|
|
|
const original = (await callWASM("ofdgoEditorFont", id)).bytes;
|
|
|
|
|
const [source] = await callWASM("ofdgoFontFaces", original);
|
|
|
|
|
if (!source?.postscriptName) throw error;
|
|
|
|
|
if (!source?.postscriptName || !source.familyKey) throw error;
|
|
|
|
|
await fontManager.loadFaces(data => callWASM("ofdgoFontFaces", data));
|
|
|
|
|
const candidates = fontManager.editorFonts().filter(font => !font.disabled && font.postscriptName === source.postscriptName);
|
|
|
|
|
const candidates = fontManager.editorFonts().filter(font => !font.disabled && font.postscriptName === source.postscriptName && font.familyKey === source.familyKey);
|
|
|
|
|
let recovered;
|
|
|
|
|
for (const choice of candidates) {
|
|
|
|
|
try {
|
|
|
|
|
const data = await readTextFont(choice, tab);
|
|
|
|
|
const [face] = await callWASM("ofdgoFontFaces", data);
|
|
|
|
|
if (face?.postscriptName !== source.postscriptName) continue;
|
|
|
|
|
if (face?.postscriptName !== source.postscriptName || face.familyKey !== source.familyKey) continue;
|
|
|
|
|
await callWASM("ofdgoCheckTextFont", data, model.runs.filter(run => run.format.font === id).map(run => run.text).join(""));
|
|
|
|
|
recovered = { key: id, data, choice };
|
|
|
|
|
break;
|
|
|
|
@@ -4376,8 +4394,9 @@ async function validateRichText(item, model, width, fonts = []) {
|
|
|
|
|
rich.imports.set(id, recovered.data);
|
|
|
|
|
rich.choices.set(id, recovered.choice);
|
|
|
|
|
const previous = rich.faces.get(id);
|
|
|
|
|
if (previous) document.fonts.delete(previous);
|
|
|
|
|
rich.faces.set(id, face); document.fonts.add(face);
|
|
|
|
|
rich.faces.set(id, face);
|
|
|
|
|
if (previous && ![...rich.faces.values()].includes(previous)) document.fonts.delete(previous);
|
|
|
|
|
document.fonts.add(face);
|
|
|
|
|
if (!editing.composing && !editing.saving) { rich.selection(); rich.render(); updateObjectControls(canvasEditor.selected); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -4443,9 +4462,10 @@ async function beginCanvasText(index, box, surface, page) {
|
|
|
|
|
const item = { ...state.textDefaults, ...box, wrap: Boolean(box.wrap),
|
|
|
|
|
index, page, surface, node, text: "", draft: true, fontData: data };
|
|
|
|
|
const model = { ...richTextModel(item), autoWidth: !box.wrap };
|
|
|
|
|
const key = `temp-font-${crypto.randomUUID()}`;
|
|
|
|
|
const key = font.embedded ? font.id.slice(9) : `temp-font-${crypto.randomUUID()}`;
|
|
|
|
|
const embedded = font.embedded && state.doc?.fonts?.some(font => font.id === key && font.embedded);
|
|
|
|
|
model.runs[0].format.font = key;
|
|
|
|
|
canvasEditor.editText(item, face, null, { model, faces: new Map([[key, face]]), fonts: [{ key, data }] });
|
|
|
|
|
canvasEditor.editText(item, face, null, { model, faces: new Map([[key, face]]), fonts: embedded ? [] : [{ key, data }], resources: new Map([[key, data]]) });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (isForegroundDocument(tab) && openSeq === state.openSeq) showError(err, false);
|
|
|
|
|
} finally {
|
|
|
|
@@ -4608,8 +4628,12 @@ async function prepareTextFaces(tab, runs, font, key, value, rich) {
|
|
|
|
|
const candidates = [...choices.values()];
|
|
|
|
|
for (const choice of candidates) {
|
|
|
|
|
if (choice.menuFamily || ![...originals.values()].some(font => textFontIdentity(font || {}) === textFontIdentity(choice)) && (!font || textFontIdentity(choice) !== textFontIdentity(font))) continue;
|
|
|
|
|
const [metadata] = await callWASM("ofdgoFontFaces", await read(choice));
|
|
|
|
|
if (metadata) Object.assign(choice, metadata, { index: choice.index });
|
|
|
|
|
try {
|
|
|
|
|
const [metadata] = await callWASM("ofdgoFontFaces", await read(choice));
|
|
|
|
|
if (metadata) Object.assign(choice, metadata, { index: choice.index });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (!current() || !font || textFontIdentity(choice) === textFontIdentity(font)) throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const groups = await groupTextFonts(candidates, tab), changes = new Map(), prepared = new Map();
|
|
|
|
|
for (const run of runs) {
|
|
|
|
@@ -4675,7 +4699,9 @@ async function changeTextFace(font, key, value) {
|
|
|
|
|
tab.editTask = busy;
|
|
|
|
|
setBusy(true, "", 0, "", tab);
|
|
|
|
|
try {
|
|
|
|
|
const fonts = fontManager.editorFonts().filter(font => font.familyKey === source.familyKey && textFontSource(font) === textFontSource(source)), groups = await groupTextFonts(fonts, tab);
|
|
|
|
|
const available = source.embedded ? (state.doc?.fonts || []).filter(font => font.embedded || font.status === "matched").map(font => documentFontChoice(font.id)) : fontManager.editorFonts();
|
|
|
|
|
const fonts = [source, ...available.filter(font => textFontIdentity(font) !== textFontIdentity(source))].filter(font => font.familyKey === source.familyKey && textFontSource(font) === textFontSource(source));
|
|
|
|
|
const groups = await groupTextFonts(fonts, tab);
|
|
|
|
|
if (!current()) return;
|
|
|
|
|
const index = fonts.findIndex(choice => textFontIdentity(choice) === textFontIdentity(source));
|
|
|
|
|
const faces = (groups.find(group => group.includes(index)) || []).map(index => fonts[index]);
|
|
|
|
@@ -4726,6 +4752,7 @@ async function changeTextFace(font, key, value) {
|
|
|
|
|
rich.faces.set(entry.key, entry.face);
|
|
|
|
|
if (previous && ![...rich.faces.values()].includes(previous)) document.fonts.delete(previous);
|
|
|
|
|
if (!entry.choice.embedded) rich.imports.set(entry.key, entry.data);
|
|
|
|
|
else rich.resources.set(entry.key, entry.data);
|
|
|
|
|
rich.choices.set(entry.key, entry.choice);
|
|
|
|
|
document.fonts.add(entry.face);
|
|
|
|
|
}
|
|
|
|
@@ -10842,8 +10869,9 @@ async function selectDocumentText() {
|
|
|
|
|
|
|
|
|
|
function copyEditorSelection(event) {
|
|
|
|
|
const tab = documentTab(), state = tab.state;
|
|
|
|
|
if (!state.editing || !canvasEditor.enabled || !canvasEditor.selected || !el.viewerPanel.contains(event.target)
|
|
|
|
|
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("data-busy")) return false;
|
|
|
|
|
if (!state.editing || !canvasEditor.enabled || !canvasEditor.selected || !editorShortcutTarget(event.target)
|
|
|
|
|
|| !el.viewerPanel.contains(event.target) && !el.editorTools.contains(event.target)
|
|
|
|
|
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("data-busy") || formDialogOpen()) return false;
|
|
|
|
|
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
|
|
|
|
|
const cut = event.type === "cut";
|
|
|
|
|
if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) {
|
|
|
|
@@ -10919,7 +10947,7 @@ function visiblePageBounds(index) {
|
|
|
|
|
|
|
|
|
|
async function pasteEditorContent(event) {
|
|
|
|
|
const tab = documentTab(), state = tab.state;
|
|
|
|
|
if (!state.editing || event.target.closest?.("input, textarea, select, [contenteditable]") || formDialogOpen()) return;
|
|
|
|
|
if (!state.editing || !editorShortcutTarget(event.target) || formDialogOpen()) return;
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const token = event.clipboardData.getData(OBJECT_CLIPBOARD_TYPE);
|
|
|
|
|
const files = Array.from(event.clipboardData.files);
|
|
|
|
@@ -10988,7 +11016,7 @@ async function pasteEditorContent(event) {
|
|
|
|
|
|
|
|
|
|
function copySelection(event) {
|
|
|
|
|
if (sealPreview) return;
|
|
|
|
|
if (event.target.closest?.("input, textarea, [contenteditable]")) {
|
|
|
|
|
if (!editorShortcutTarget(event.target)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const selection = document.getSelection();
|
|
|
|
@@ -11033,7 +11061,14 @@ function copySelection(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function focusSearch() {
|
|
|
|
|
async function focusSearch(direction = 0) {
|
|
|
|
|
if (canvasEditor.input || canvasEditor.crop || canvasEditor.nudge || canvasEditor.nudgeCommit) {
|
|
|
|
|
const tab = documentTab(), content = documentContentKey(state);
|
|
|
|
|
const current = () => isForegroundDocument(tab) && content === documentContentKey(state);
|
|
|
|
|
if (!await canvasEditor.commitNudge() || !current() || !await canvasEditor.commitText() || !current()
|
|
|
|
|
|| !await canvasEditor.commitCrop() || !current() || document.body.hasAttribute("data-busy") || formDialogOpen()) return;
|
|
|
|
|
}
|
|
|
|
|
if (direction && state.searchMatches.length) return selectSearchMatch((direction < 0 ? Math.max(0, state.searchIndex) : state.searchIndex) + direction);
|
|
|
|
|
if (!state.showPages) {
|
|
|
|
|
toggleSidebar("pages");
|
|
|
|
|
}
|
|
|
|
@@ -12737,6 +12772,30 @@ function updateEditorTools() {
|
|
|
|
|
updateObjectControls(canvasEditor.selected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateObjectDistribution(item, disabled) {
|
|
|
|
|
const tab = documentTab(), state = tab.state;
|
|
|
|
|
const ids = item?.items?.slice().sort((a, b) => a.order - b.order).map(member => member.id) || [];
|
|
|
|
|
if (disabled || ids.length < 3) state.objectDistribution = null;
|
|
|
|
|
else {
|
|
|
|
|
const { openSeq, wasmSeq } = state, revision = state.editorInfo?.revision;
|
|
|
|
|
const key = JSON.stringify([openSeq, wasmSeq, revision, item.index, ids]);
|
|
|
|
|
if (state.objectDistribution?.key !== key) {
|
|
|
|
|
const request = state.objectDistribution = { key, axes: [] };
|
|
|
|
|
const current = () => !tab.closed && state.objectDistribution === request && state.openSeq === openSeq && state.wasmSeq === wasmSeq && state.editorInfo?.revision === revision;
|
|
|
|
|
callTabWASM(tab, "ofdgoDistributionAxes", item.index, ids).then(axes => {
|
|
|
|
|
if (!current()) return;
|
|
|
|
|
request.axes = axes;
|
|
|
|
|
if (isForegroundDocument(tab)) updateObjectControls(canvasEditor.selected);
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
if (current() && isForegroundDocument(tab)) showError(error, false);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const axes = state.objectDistribution?.axes || [];
|
|
|
|
|
el.objectDistribute.disabled = disabled || !axes.length;
|
|
|
|
|
for (const option of el.objectDistribute.options) if (option.value) option.disabled = !axes.includes(option.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateObjectControls(item, reset = false) {
|
|
|
|
|
const members = item?.items || (item ? [item] : []);
|
|
|
|
|
const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting;
|
|
|
|
@@ -12754,7 +12813,7 @@ function updateObjectControls(item, reset = false) {
|
|
|
|
|
el.cropImageButton.setAttribute("data-pressed", String(cropping));
|
|
|
|
|
el.resetCropButton.textContent = cropping ? "取消" : "还原";
|
|
|
|
|
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;
|
|
|
|
|
updateObjectDistribution(item, el.objectAlign.disabled);
|
|
|
|
|
el.editObjectButton.disabled = disabled || (item.items ? !item.items.every(member => member.type === "TextObject" && !member.richText)
|
|
|
|
|
: !canEditObject(item, "children") && (item.type === "PathObject"
|
|
|
|
|
|| !(item.type === "TextObject" && canEditObject(item, "rewriteText")) && !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update")));
|
|
|
|
|