Compare commits

..
6 Commits
Author SHA1 Message Date
xiaoqidun 838a769c5b fix(编辑能力): 修复输入状态下标签快捷键
build.yaml / build (push) Successful in 47s
2026-10-29 15:01:52 +08:00
xiaoqidun 5b4f2472f7 fix(编辑能力): 修复快捷键遗漏及跨文档干扰
build.yaml / build (push) Successful in 45s
2026-10-29 14:52:40 +08:00
xiaoqidun fb51fd3b07 fix(编辑能力): 修复工具栏复制剪切粘贴失效
build.yaml / build (push) Successful in 45s
2026-10-29 14:30:58 +08:00
xiaoqidun 80e548f93e fix(编辑能力): 修复工具栏撤销快捷键失效
build.yaml / build (push) Successful in 48s
2026-10-29 14:16:31 +08:00
xiaoqidun 9c3a04b7eb feat(编辑能力): 优化分布操作,提前禁用无效方向
build.yaml / build (push) Successful in 46s
2026-10-29 13:54:58 +08:00
xiaoqidun 9bb6c9141a feat(基础镜像): 升级到alpine:3.24.3
build.yaml / build (push) Successful in 44s
2026-10-29 08:46:31 +08:00
6 changed files with 106 additions and 14 deletions

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
# 基础镜像 # 基础镜像
FROM alpine:3.24.2 FROM alpine:3.24.3
# 作者信息 # 作者信息
LABEL authors="肖其顿 (XIAO QI DUN)" LABEL authors="肖其顿 (XIAO QI DUN)"
+52 -9
View File
@@ -110,6 +110,7 @@ function createDocumentState() {
styleOriginal: null, styleOriginal: null,
objectClipboardToken: null, objectClipboardToken: null,
styleClipboard: null, styleClipboard: null,
objectDistribution: null,
outlineExpanded: new Map(), outlineExpanded: new Map(),
textFonts: [], textFonts: [],
textFontID: null, textFontID: null,
@@ -1260,7 +1261,7 @@ el.objectAlign.addEventListener("change", () => {
el.objectDistribute.addEventListener("change", () => { el.objectDistribute.addEventListener("change", () => {
const axis = el.objectDistribute.value; const axis = el.objectDistribute.value;
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); return changeDocument("ofdgoDistributeObjects", canvasEditor.selected, axis);
} }
}); });
@@ -1307,7 +1308,10 @@ for (const [button, tool] of [[el.drawLineButton, "line"], [el.drawRectangleButt
} }
editorClick(el.eraseButton, () => toggleDrawingTool(el.eraseMode.value)); editorClick(el.eraseButton, () => toggleDrawingTool(el.eraseMode.value));
el.eraseMode.addEventListener("change", () => { 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]) { for (const input of [el.shapeFill, el.shapeStroke]) {
input.addEventListener("change", changeShapeStyle); input.addEventListener("change", changeShapeStyle);
@@ -1407,9 +1411,11 @@ for (const [button, ungroup] of [[el.groupObjectsButton, false], [el.ungroupObje
}); });
} }
el.arrowTool.addEventListener("change", async () => { 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; const tool = el.arrowTool.value;
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; const item = canvasEditor.selected;
if (lineShape(item?.shape)) { if (lineShape(item?.shape)) {
const box = item.geometry; const box = item.geometry;
@@ -1420,6 +1426,7 @@ el.arrowTool.addEventListener("change", async () => {
state.selectObjects = true; state.selectObjects = true;
canvasEditor.setTool(tool); canvasEditor.setTool(tool);
updateControls(); updateControls();
canvasEditor.focus();
} }
}); });
el.copyStyleButton.addEventListener("click", async () => { el.copyStyleButton.addEventListener("click", async () => {
@@ -1807,6 +1814,7 @@ el.annotationButton.addEventListener("click", toggleAnnotations);
document.addEventListener("copy", copySelection); document.addEventListener("copy", copySelection);
document.addEventListener("cut", copySelection); document.addEventListener("cut", copySelection);
el.viewerPanel.addEventListener("paste", pasteEditorContent); el.viewerPanel.addEventListener("paste", pasteEditorContent);
el.editorTools.addEventListener("paste", pasteEditorContent);
document.addEventListener("selectionchange", syncSelection); document.addEventListener("selectionchange", syncSelection);
el.exportFormat.addEventListener("change", () => updateDPIControl()); el.exportFormat.addEventListener("change", () => updateDPIControl());
el.outputCompression.addEventListener("change", () => { el.outputCompression.addEventListener("change", () => {
@@ -1933,6 +1941,11 @@ el.documentNewTab.addEventListener("click", openEmptyDocumentTab);
updateSidebarState(); updateSidebarState();
boot(); 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) { function handleKeyDown(event) {
if (sealPreview) { if (sealPreview) {
if (event.key === "Escape") { event.preventDefault(); finishSealPosition(); } if (event.key === "Escape") { event.preventDefault(); finishSealPosition(); }
@@ -1994,7 +2007,7 @@ function handleKeyDown(event) {
} }
} }
if (event.ctrlKey || event.metaKey) { 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"))) { && (key.toLowerCase() === "z" || (!event.shiftKey && key.toLowerCase() === "y"))) {
event.preventDefault(); event.preventDefault();
(key.toLowerCase() === "y" || event.shiftKey ? el.redoButton : el.undoButton).click(); (key.toLowerCase() === "y" || event.shiftKey ? el.redoButton : el.undoButton).click();
@@ -2010,6 +2023,11 @@ function handleKeyDown(event) {
if (!state.doc) { if (!state.doc) {
return; 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) if (key === "Escape" && !event.shiftKey && state.documentSelection && el.viewerPanel.contains(target)
&& !target.closest("input, textarea, select, [contenteditable]")) { && !target.closest("input, textarea, select, [contenteditable]")) {
event.preventDefault(); event.preventDefault();
@@ -10851,8 +10869,9 @@ async function selectDocumentText() {
function copyEditorSelection(event) { function copyEditorSelection(event) {
const tab = documentTab(), state = tab.state; const tab = documentTab(), state = tab.state;
if (!state.editing || !canvasEditor.enabled || !canvasEditor.selected || !el.viewerPanel.contains(event.target) if (!state.editing || !canvasEditor.enabled || !canvasEditor.selected || !editorShortcutTarget(event.target)
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("data-busy")) return false; || !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 items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
const cut = event.type === "cut"; const cut = event.type === "cut";
if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) { if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) {
@@ -10928,7 +10947,7 @@ function visiblePageBounds(index) {
async function pasteEditorContent(event) { async function pasteEditorContent(event) {
const tab = documentTab(), state = tab.state; 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(); event.preventDefault();
const token = event.clipboardData.getData(OBJECT_CLIPBOARD_TYPE); const token = event.clipboardData.getData(OBJECT_CLIPBOARD_TYPE);
const files = Array.from(event.clipboardData.files); const files = Array.from(event.clipboardData.files);
@@ -10997,7 +11016,7 @@ async function pasteEditorContent(event) {
function copySelection(event) { function copySelection(event) {
if (sealPreview) return; if (sealPreview) return;
if (event.target.closest?.("input, textarea, [contenteditable]")) { if (!editorShortcutTarget(event.target)) {
return; return;
} }
const selection = document.getSelection(); const selection = document.getSelection();
@@ -12746,6 +12765,30 @@ function updateEditorTools() {
updateObjectControls(canvasEditor.selected); 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) { function updateObjectControls(item, reset = false) {
const members = item?.items || (item ? [item] : []); const members = item?.items || (item ? [item] : []);
const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting; const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting;
@@ -12763,7 +12806,7 @@ function updateObjectControls(item, reset = false) {
el.cropImageButton.setAttribute("data-pressed", String(cropping)); el.cropImageButton.setAttribute("data-pressed", String(cropping));
el.resetCropButton.textContent = 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.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) el.editObjectButton.disabled = disabled || (item.items ? !item.items.every(member => member.type === "TextObject" && !member.richText)
: !canEditObject(item, "children") && (item.type === "PathObject" : !canEditObject(item, "children") && (item.type === "PathObject"
|| !(item.type === "TextObject" && canEditObject(item, "rewriteText")) && !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update"))); || !(item.type === "TextObject" && canEditObject(item, "rewriteText")) && !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update")));
+3 -3
View File
@@ -1741,8 +1741,8 @@ export class CanvasEditor {
return this.nudgeCommit; return this.nudgeCommit;
} }
keyDown(event) { keyDown(event, toolbar = false) {
if (event.defaultPrevented || event.target?.closest("input, textarea, select, [contenteditable]")) return; if (event.defaultPrevented || !toolbar && event.target?.closest("input, textarea, select, [contenteditable]")) return;
if (this.crop && !this.options.busy()) { if (this.crop && !this.options.busy()) {
if (event.key === "Escape" || event.key === "Enter") { if (event.key === "Escape" || event.key === "Enter") {
event.preventDefault(); event.preventDefault();
@@ -1952,7 +1952,7 @@ export class CanvasEditor {
} }
}); });
input.addEventListener("keydown", (event) => { input.addEventListener("keydown", (event) => {
if ((event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "s") return; if ((event.ctrlKey || event.metaKey) && !event.altKey && ["s", "w", "tab"].includes(event.key.toLowerCase())) return;
event.stopPropagation(); event.stopPropagation();
if (event.isComposing || editing.composing || editing.saving) { if (event.isComposing || editing.composing || editing.saving) {
return; return;
+1 -1
View File
@@ -183,7 +183,7 @@ export class FontPicker {
} }
keyDown(event) { keyDown(event) {
if (event.isComposing) return; if (event.isComposing || event.key === "Tab" && (event.ctrlKey || event.metaKey || event.altKey)) return;
if (!["Escape", "Enter", "ArrowDown", "ArrowUp", "Tab"].includes(event.key)) return; if (!["Escape", "Enter", "ArrowDown", "ArrowUp", "Tab"].includes(event.key)) return;
event.stopPropagation(); event.stopPropagation();
if (event.key === "Escape") { if (event.key === "Escape") {
+15
View File
@@ -5703,6 +5703,20 @@ func (e *businessEngine) orderObjects(args []platformValue) (any, error) {
}) })
} }
// distributionAxes 获取画布选区可用的等间距分布方向
// 入参: args 页面索引和画布对象标识数组
// 返回: any 可用方向, error 路径或度量错误
func (e *businessEngine) distributionAxes(args []platformValue) (any, error) {
if e.currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
paths, err := e.editorTargetPaths(e.stringsFromJS(args[1]))
if err != nil {
return nil, err
}
return e.currentEditor.DistributionAxes(args[0].Int(), paths)
}
// distributeObjects 按指定轴等距分布选区 // distributeObjects 按指定轴等距分布选区
// 入参: args 页面索引、对象标识数组和分布轴 // 入参: args 页面索引、对象标识数组和分布轴
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
@@ -8159,6 +8173,7 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
"ofdgoImportObjects": e.importObjects, "ofdgoImportObjects": e.importObjects,
"ofdgoOrderObjects": e.orderObjects, "ofdgoOrderObjects": e.orderObjects,
"ofdgoDistributeObjects": e.distributeObjects, "ofdgoDistributeObjects": e.distributeObjects,
"ofdgoDistributionAxes": e.distributionAxes,
"ofdgoRotateObjects": e.rotateObjects, "ofdgoRotateObjects": e.rotateObjects,
"ofdgoFlipObjects": e.flipObjects, "ofdgoFlipObjects": e.flipObjects,
"ofdgoCropImage": e.cropImage, "ofdgoCropImage": e.cropImage,
+34
View File
@@ -808,6 +808,40 @@ func (e *Editor) ResizeTargets(page int, paths []ObjectPath, box Box) error {
return e.TransformTargets(page, paths, Matrix{a: sx, d: sy, e: box.X - bounds.X*sx, f: box.Y - bounds.Y*sy}) return e.TransformTargets(page, paths, Matrix{a: sx, d: sy, e: box.X - bounds.X*sx, f: box.Y - bounds.Y*sy})
} }
// DistributionAxes 获取选区可保持位置顺序的等间距分布方向,不修改文档或历史
// 入参: page 页面索引, paths 按绘制顺序排列的完整路径
// 返回: []string 可用的horizontal和vertical方向, error 路径或度量错误
func (e *Editor) DistributionAxes(page int, paths []ObjectPath) ([]string, error) {
objects, annotations, groups, err := editorTargetGroups(paths)
if err != nil {
return nil, err
}
var boxes []Box
var positions []editorObjectPosition
if len(groups) == 0 {
boxes, positions, _, err = e.selectionGeometry(page, objects, annotations)
} else {
boxes, _, _, err = e.targetGeometry(page, paths)
positions = make([]editorObjectPosition, len(paths))
for i := range positions {
positions[i].index = i
}
}
if err != nil {
return nil, err
}
axes := []string{}
if len(boxes) < 3 {
return axes, nil
}
for _, axis := range []string{"horizontal", "vertical"} {
if _, err := editorDistribution(boxes, positions, axis); err == nil {
axes = append(axes, axis)
}
}
return axes, nil
}
// ArrangeTargets 对齐或等距分布跨容器选区,单选对齐页面 // ArrangeTargets 对齐或等距分布跨容器选区,单选对齐页面
// 入参: page 页面索引, paths 按绘制顺序排列的完整路径, distribute 是否分布, direction 对齐方向或分布轴 // 入参: page 页面索引, paths 按绘制顺序排列的完整路径, distribute 是否分布, direction 对齐方向或分布轴
// 返回: error 路径、方向或编辑错误 // 返回: error 路径、方向或编辑错误