Skip to content

Commit 876eda0

Browse files
author
jay
committed
feat: table 编辑
1 parent 6735456 commit 876eda0

12 files changed

Lines changed: 798 additions & 10 deletions

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,26 @@ patches back to the selected local file automatically. Rapid changes are batched
103103
after 1.2 seconds of inactivity, with a 5 second maximum wait during continuous
104104
adjustments. File switching and Reload flush pending changes before continuing.
105105

106+
## Table Editing
107+
108+
Text inside table cells uses the same inline text and range-formatting tools as
109+
other page text. When no text range is active, selecting content inside a
110+
regular rectangular table replaces the element duplicate/delete menu with row
111+
and column actions:
112+
113+
- insert a row above or below
114+
- insert a column to the left or right
115+
- delete the current row or column
116+
117+
New rows and columns preserve the presentation attributes of the neighboring
118+
row or cell, start with empty content, and do not copy element IDs. The preview
119+
updates immediately and the same semantic operation is applied to the source
120+
HTML during auto-save.
121+
122+
Structural editing is deliberately disabled for tables that use `rowspan`,
123+
`colspan`, `colgroup`, or inconsistent cell counts. Their cell text remains
124+
editable, but changing their grid safely requires a span-aware table model.
125+
106126
## Publishing
107127

108128
Publishing is handled by [`.github/workflows/publish.yml`](.github/workflows/publish.yml).

htmlPatch.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { parseDocument } from "htmlparser2";
22
import { selectAll } from "css-select";
3+
import { patchTableElementHtml } from "./tablePatch.js";
34

45
const allowedStyleProperties = new Set([
56
"box-sizing",
@@ -476,6 +477,12 @@ function patchElementHtml(elementHtml, node, operations) {
476477
continue;
477478
}
478479

480+
if (String(operation.type || "").startsWith("table-")
481+
&& operations.length === 1
482+
&& node.name === "table") {
483+
return patchTableElementHtml(next, node, operation);
484+
}
485+
479486
// Structural operations replace the resolved source range and therefore
480487
// must be the final operation in one patch request.
481488
if (operation.type === "duplicate-element" && index === operations.length - 1) {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"projectContext.js",
3131
"public",
3232
"server.js",
33+
"tablePatch.js",
3334
"README.md"
3435
],
3536
"scripts": {

public/app.js

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import {
1414
shouldCommitInlineEdit,
1515
shouldEnsureFixedWidthWrap,
1616
} from "./inlineEdit.js?v=editor-interactions-v4";
17-
import { createCanvasTextEditor } from "./canvasTextEditor.js?v=editor-interactions-v3";
17+
import { createCanvasTextEditor } from "./canvasTextEditor.js?v=editor-interactions-v5";
1818
import { createAutoSaveController } from "./autoSaveController.js";
1919
import { positionStart } from "./canvasEditorMath.js";
2020
import { createEditorHistory } from "./editorHistory.js";
2121
import { appendStructuralPatch, appendStylePatch } from "./patchQueue.js";
22-
import { dragTargetAtPoint } from "./pointerIntent.js?v=editor-interactions-v3";
22+
import { dragTargetAtPoint } from "./pointerIntent.js?v=editor-interactions-v4";
2323
import { activateEmbeddedPreview } from "./previewLifecycle.js";
2424
import {
2525
injectPreviewBase,
@@ -32,6 +32,10 @@ import {
3232
planTextFieldContentOperations,
3333
textStructureSignature,
3434
} from "./textFieldModel.js?v=editor-interactions-v4";
35+
import {
36+
applyTableAction,
37+
tableContextForElement,
38+
} from "./tableEditing.js?v=table-editing-v1";
3539

3640
// state 保存编辑器运行时状态;真正的 HTML 内容仍然在 iframe 文档里。
3741
const state = {
@@ -764,6 +768,27 @@ function injectEditorLayer() {
764768
markDirty();
765769
clearSelection();
766770
},
771+
onTableAction(action, context) {
772+
const tableTarget = targetForElement(context.table);
773+
const result = applyTableAction(context, action);
774+
if (!result.changed) {
775+
setStatus(result.reason || "This table operation is unavailable", "error");
776+
state.canvasEditor?.refresh();
777+
return;
778+
}
779+
780+
queueStructuralPatch(tableTarget, result.operation);
781+
// A table operation changes its text fingerprint and DOM paths. Cache the
782+
// new identity so a second operation in the same auto-save batch resolves
783+
// against the source produced by the first operation.
784+
state.elementTargets.set(
785+
context.table,
786+
createElementTarget(context.table, selectorFor(context.table)),
787+
);
788+
markDirty();
789+
if (result.selectedCell?.isConnected) selectElement(result.selectedCell);
790+
else clearSelection();
791+
},
767792
onSelectionChange() {
768793
syncInspector();
769794
},
@@ -839,7 +864,8 @@ function injectEditorLayer() {
839864
applyDragMarker(dragTarget);
840865
return;
841866
}
842-
const target = resolveEditableTextTarget(event.target);
867+
const tableCell = event.target.closest?.("td, th");
868+
const target = resolveEditableTextTarget(event.target) || tableCell;
843869
if (!target) {
844870
event.preventDefault();
845871
event.stopImmediatePropagation();
@@ -950,18 +976,22 @@ function handleEditorShortcut(event) {
950976
}
951977
const element = selectedElement();
952978
if (!element || isFormControl(event.target)) return;
979+
const tableContext = tableContextForElement(element);
953980

954981
if (modifier && key === "d") {
955982
event.preventDefault();
983+
if (tableContext) return;
956984
state.canvasEditor?.duplicateSelected();
957985
return;
958986
}
959987
if (event.key === "Delete" || event.key === "Backspace") {
960988
event.preventDefault();
989+
if (tableContext) return;
961990
state.canvasEditor?.deleteSelected();
962991
return;
963992
}
964993
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) {
994+
if (tableContext) return;
965995
event.preventDefault();
966996
recordHistory();
967997
const step = event.shiftKey ? 10 : 1;

0 commit comments

Comments
 (0)