@portone/docx-editor 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/dist/DocxEditor.js +84 -26
- package/dist/core.d.ts +4 -3
- package/dist/docx/exportDocx.d.ts +3 -2
- package/dist/docx/exportDocx.js +6 -10
- package/dist/docx/headersFooters.js +35 -23
- package/dist/docx/identities.d.ts +30 -0
- package/dist/docx/identities.js +43 -10
- package/dist/docx/importDocx.js +25 -26
- package/dist/docx/importParagraph.d.ts +1 -4
- package/dist/docx/importParagraph.js +4 -18
- package/dist/docx/invariants.d.ts +13 -10
- package/dist/docx/invariants.js +300 -116
- package/dist/docx/newLists.js +12 -3
- package/dist/docx/notes/newNote.d.ts +39 -0
- package/dist/docx/notes/newNote.js +122 -0
- package/dist/docx/notes/numbering.d.ts +27 -0
- package/dist/docx/notes/numbering.js +113 -0
- package/dist/docx/notes/reading.d.ts +58 -0
- package/dist/docx/notes/reading.js +98 -0
- package/dist/docx/notes/references.d.ts +33 -0
- package/dist/docx/notes/references.js +37 -0
- package/dist/docx/notes/writing.d.ts +19 -0
- package/dist/docx/notes/writing.js +95 -0
- package/dist/docx/partPlan.d.ts +3 -0
- package/dist/docx/partPlanners.d.ts +13 -0
- package/dist/docx/partPlanners.js +48 -0
- package/dist/docx/sections.d.ts +5 -0
- package/dist/docx/sections.js +7 -2
- package/dist/docx/serializeParagraph.js +7 -2
- package/dist/docx/serializePreserved.d.ts +5 -1
- package/dist/docx/serializePreserved.js +24 -7
- package/dist/docx/session.d.ts +5 -0
- package/dist/docx/session.js +6 -0
- package/dist/docx/storyParts.d.ts +133 -0
- package/dist/docx/storyParts.js +314 -0
- package/dist/editor/clipboard/htmlReader.d.ts +24 -4
- package/dist/editor/clipboard/htmlReader.js +39 -4
- package/dist/editor/clipboard/internalChannel.d.ts +30 -3
- package/dist/editor/clipboard/internalChannel.js +43 -3
- package/dist/editor/clipboard/normalizers.d.ts +35 -8
- package/dist/editor/clipboard/normalizers.js +60 -11
- package/dist/editor/clipboard/plugin.js +42 -22
- package/dist/editor/commands/exportQueries.d.ts +4 -3
- package/dist/editor/commands/index.d.ts +2 -1
- package/dist/editor/commands/index.js +18 -0
- package/dist/editor/commands/noteCommands.d.ts +47 -0
- package/dist/editor/commands/noteCommands.js +95 -0
- package/dist/editor/commands/noteQueries.d.ts +22 -3
- package/dist/editor/commands/noteQueries.js +21 -13
- package/dist/editor/createEditor.js +21 -3
- package/dist/editor/documentStyles.d.ts +3 -0
- package/dist/editor/documentStyles.js +2 -1
- package/dist/editor/editorDocument.d.ts +24 -0
- package/dist/editor/editorDocument.js +34 -4
- package/dist/editor/notes/noteSurface.d.ts +53 -0
- package/dist/editor/notes/noteSurface.js +246 -0
- package/dist/editor/plugins/compositionSelection.d.ts +20 -0
- package/dist/editor/plugins/compositionSelection.js +19 -0
- package/dist/editor/plugins/keymap.d.ts +14 -0
- package/dist/editor/plugins/keymap.js +13 -1
- package/dist/editor/plugins/noteLifecycle.d.ts +18 -0
- package/dist/editor/plugins/noteLifecycle.js +117 -0
- package/dist/editor/plugins/noteNavigation.d.ts +45 -0
- package/dist/editor/plugins/noteNavigation.js +86 -0
- package/dist/editor/plugins/noteNumbering.d.ts +13 -0
- package/dist/editor/plugins/noteNumbering.js +58 -0
- package/dist/editor/plugins/rowResize.js +2 -7
- package/dist/editor/plugins/tabLayout.js +2 -7
- package/dist/editor/stories/storyMarkup.d.ts +24 -0
- package/dist/editor/stories/storyMarkup.js +35 -0
- package/dist/editor/stories/storyState.d.ts +34 -0
- package/dist/editor/stories/storyState.js +74 -0
- package/dist/editor/stories/storyView.d.ts +115 -0
- package/dist/editor/stories/storyView.js +144 -0
- package/dist/index.d.ts +2 -2
- package/dist/numbering/spellers.d.ts +1 -1
- package/dist/numbering/spellers.js +6 -1
- package/dist/ooxml/errors.d.ts +135 -2
- package/dist/ooxml/errors.js +6 -0
- package/dist/page/blockKinds.d.ts +23 -4
- package/dist/page/demands/footnoteDemands.d.ts +18 -0
- package/dist/page/demands/footnoteDemands.js +51 -0
- package/dist/page/demands/index.d.ts +43 -0
- package/dist/page/demands/index.js +8 -0
- package/dist/page/kinds/paragraphKind.js +7 -1
- package/dist/page/kinds/tableKind.js +9 -1
- package/dist/page/measureBlocks.d.ts +2 -0
- package/dist/page/measureBlocks.js +31 -10
- package/dist/page/pageDecorations.d.ts +9 -2
- package/dist/page/pageDecorations.js +18 -5
- package/dist/page/pageLayout.d.ts +77 -1
- package/dist/page/pageLayout.js +186 -9
- package/dist/page/usePageLayout.d.ts +57 -2
- package/dist/page/usePageLayout.js +50 -5
- package/dist/schema/attrRoles.js +3 -2
- package/dist/schema/docxSchema.js +5 -2
- package/dist/schema/editGuard.d.ts +1 -1
- package/dist/schema/guards.js +1 -2
- package/dist/schema/preservedGuards.d.ts +4 -6
- package/dist/schema/preservedGuards.js +0 -9
- package/dist/schema/stories.d.ts +42 -2
- package/dist/schema/stories.js +28 -5
- package/dist/styles/classNames.d.ts +21 -6
- package/dist/styles/classNames.js +21 -6
- package/dist/styles/inlineStyle.d.ts +7 -0
- package/dist/styles/inlineStyle.js +11 -5
- package/dist/styles/visualScale.d.ts +19 -0
- package/dist/styles/visualScale.js +26 -0
- package/dist/styles.css +89 -32
- package/dist/ui/InsertImageButton.d.ts +3 -1
- package/dist/ui/InsertImageButton.js +3 -2
- package/dist/ui/TableMenu.d.ts +5 -2
- package/dist/ui/TableMenu.js +18 -1
- package/dist/ui/TextMenu.d.ts +5 -2
- package/dist/ui/TextMenu.js +21 -6
- package/dist/ui/Toolbar.d.ts +17 -2
- package/dist/ui/Toolbar.js +23 -10
- package/dist/ui/comments/useCommentRailLayout.js +2 -4
- package/dist/ui/noteItems.d.ts +21 -0
- package/dist/ui/noteItems.js +30 -0
- package/dist/ui/notes/NoteAreas.d.ts +73 -0
- package/dist/ui/notes/NoteAreas.js +131 -0
- package/dist/ui/notes/NoteList.d.ts +25 -0
- package/dist/ui/notes/NoteList.js +58 -0
- package/dist/ui/notes/StoryRow.d.ts +75 -0
- package/dist/ui/notes/StoryRow.js +122 -0
- package/dist/ui/notes/noteBands.d.ts +100 -0
- package/dist/ui/notes/noteBands.js +142 -0
- package/dist/ui/notes/noteHeights.d.ts +10 -0
- package/dist/ui/notes/noteHeights.js +24 -0
- package/dist/ui/notes/noteSeparator.d.ts +12 -0
- package/dist/ui/notes/noteSeparator.js +9 -0
- package/dist/ui/notes/useStorySurface.d.ts +68 -0
- package/dist/ui/notes/useStorySurface.js +95 -0
- package/dist/ui/runCommand.d.ts +8 -1
- package/dist/ui/runCommand.js +2 -2
- package/dist/ui/shortcutLabels.d.ts +16 -0
- package/dist/ui/shortcutLabels.js +9 -0
- package/dist/ui/usePageRoom.d.ts +13 -0
- package/dist/ui/usePageRoom.js +31 -0
- package/package.json +1 -1
- package/dist/docx/notes.d.ts +0 -27
- package/dist/docx/notes.js +0 -55
- package/dist/ui/NotesPanel.d.ts +0 -6
- package/dist/ui/NotesPanel.js +0 -33
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/editor/plugins/noteNumbering.ts
|
|
2
|
+
import { Plugin } from "prosemirror-state";
|
|
3
|
+
import { DocAttrStep } from "prosemirror-transform";
|
|
4
|
+
import { noteLabelSignature, noteLabelsIn } from "../../docx/notes/numbering.js";
|
|
5
|
+
import { sectionBreakOf } from "../../docx/sections.js";
|
|
6
|
+
import { displayOnly } from "../../schema/displayDerivation.js";
|
|
7
|
+
import { stepReaches } from "../../schema/guards.js";
|
|
8
|
+
import { documentOf } from "../editorDocument.js";
|
|
9
|
+
function countsNotes(node) {
|
|
10
|
+
if (node.type.name === "noteReference") return true;
|
|
11
|
+
const pPr = node.attrs.pPr;
|
|
12
|
+
return node.type.name === "paragraph" && typeof pPr === "string" && sectionBreakOf(pPr) !== null;
|
|
13
|
+
}
|
|
14
|
+
function reachesLabels(tr) {
|
|
15
|
+
return tr.steps.some(
|
|
16
|
+
(step, index) => step instanceof DocAttrStep ? step.attr === "sectPr" : stepReaches(
|
|
17
|
+
step,
|
|
18
|
+
tr.docs[index],
|
|
19
|
+
tr.docs[index + 1] ?? tr.doc,
|
|
20
|
+
countsNotes
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
function labelsMayHaveMoved(transactions, oldState, newState) {
|
|
25
|
+
return transactions.some(reachesLabels) && noteLabelSignature(oldState.doc) !== noteLabelSignature(newState.doc);
|
|
26
|
+
}
|
|
27
|
+
function numberingReplaced(before, after) {
|
|
28
|
+
return before.noteNumbering !== after.noteNumbering || before.specialNotes !== after.specialNotes;
|
|
29
|
+
}
|
|
30
|
+
function noteNumbering(labelsIn = noteLabelsIn) {
|
|
31
|
+
return new Plugin({
|
|
32
|
+
appendTransaction(transactions, oldState, newState) {
|
|
33
|
+
const document = documentOf(newState);
|
|
34
|
+
const replaced = numberingReplaced(documentOf(oldState), document);
|
|
35
|
+
if (!replaced && !labelsMayHaveMoved(transactions, oldState, newState)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const labels = labelsIn(
|
|
39
|
+
newState.doc,
|
|
40
|
+
document.noteNumbering,
|
|
41
|
+
document.specialNotes
|
|
42
|
+
);
|
|
43
|
+
const tr = newState.tr;
|
|
44
|
+
for (const [pos, label] of labels) {
|
|
45
|
+
if (tr.doc.nodeAt(pos)?.attrs.label !== label) {
|
|
46
|
+
tr.setNodeAttribute(pos, "label", label);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!tr.docChanged) return null;
|
|
50
|
+
tr.setMeta(displayOnly, true);
|
|
51
|
+
if (replaced) tr.setMeta("addToHistory", false);
|
|
52
|
+
return tr;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
noteNumbering
|
|
58
|
+
};
|
|
@@ -4,6 +4,7 @@ import { TableMap } from "prosemirror-tables";
|
|
|
4
4
|
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
5
5
|
import { isLockedCell } from "../../schema/locks.js";
|
|
6
6
|
import { editorClassNames } from "../../styles/classNames.js";
|
|
7
|
+
import { pageScaleAround } from "../../styles/visualScale.js";
|
|
7
8
|
import {
|
|
8
9
|
buildResizeRowTransaction,
|
|
9
10
|
resizedRowHeight,
|
|
@@ -52,12 +53,6 @@ function edgeUnder(view, event) {
|
|
|
52
53
|
if (!(rowDom instanceof HTMLTableRowElement)) return null;
|
|
53
54
|
return { ...spot, ...hit, tableDom, rowDom, rowPos };
|
|
54
55
|
}
|
|
55
|
-
function visualScale(view) {
|
|
56
|
-
const layer = view.dom.closest(`.${editorClassNames.pageLayer}`);
|
|
57
|
-
const raw = layer instanceof HTMLElement ? getComputedStyle(layer).zoom : "";
|
|
58
|
-
const parsed = Number.parseFloat(raw);
|
|
59
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
|
|
60
|
-
}
|
|
61
56
|
function previewDecorations(doc, preview) {
|
|
62
57
|
const row = doc.nodeAt(preview.rowPos);
|
|
63
58
|
if (row?.type.spec.tableRole !== "row") return DecorationSet.empty;
|
|
@@ -137,7 +132,7 @@ function rowResize() {
|
|
|
137
132
|
function start(view, event) {
|
|
138
133
|
const target = edgeUnder(view, event);
|
|
139
134
|
if (!target) return false;
|
|
140
|
-
const scale =
|
|
135
|
+
const scale = pageScaleAround(view.dom);
|
|
141
136
|
const startHeightPt = target.rowDom.getBoundingClientRect().height / scale * (72 / 96);
|
|
142
137
|
drag = {
|
|
143
138
|
view,
|
|
@@ -5,6 +5,7 @@ import { PX_PER_PT } from "../../docx/pageGeometry.js";
|
|
|
5
5
|
import { toParagraphFormat } from "../../model/format.js";
|
|
6
6
|
import { docxSchema } from "../../schema/index.js";
|
|
7
7
|
import { editorClassNames, editorCssVariables } from "../../styles/classNames.js";
|
|
8
|
+
import { pageScaleAround } from "../../styles/visualScale.js";
|
|
8
9
|
import { documentDefaultTabStopPt } from "../documentStyles.js";
|
|
9
10
|
import { setTabWidths, tabWidths } from "./tabDecorations.js";
|
|
10
11
|
var POSITION_EPSILON_PT = 0.01;
|
|
@@ -46,11 +47,6 @@ function numeric(value) {
|
|
|
46
47
|
function computedStyle(element) {
|
|
47
48
|
return element.ownerDocument.defaultView?.getComputedStyle(element) ?? getComputedStyle(element);
|
|
48
49
|
}
|
|
49
|
-
function scaleOf(view) {
|
|
50
|
-
const layer = view.dom.closest(`.${editorClassNames.pageLayer}`);
|
|
51
|
-
const parsed = Number.parseFloat(layer ? computedStyle(layer).zoom : "");
|
|
52
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
|
|
53
|
-
}
|
|
54
50
|
function paragraphLayoutContext(view, slot) {
|
|
55
51
|
const $pos = view.state.doc.resolve(view.posAtDOM(slot, 0));
|
|
56
52
|
for (let depth = $pos.depth; depth > 0; depth -= 1) {
|
|
@@ -84,7 +80,6 @@ function cloneParagraph(paragraph, widthPx, resetTabs) {
|
|
|
84
80
|
clone.style.textAlign = "start";
|
|
85
81
|
clone.style.visibility = "hidden";
|
|
86
82
|
clone.style.pointerEvents = "none";
|
|
87
|
-
clone.style.zoom = "1";
|
|
88
83
|
clone.setAttribute("aria-hidden", "true");
|
|
89
84
|
if (resetTabs) {
|
|
90
85
|
for (const slot of clone.querySelectorAll(
|
|
@@ -221,7 +216,7 @@ function sameWidths(left, right) {
|
|
|
221
216
|
return true;
|
|
222
217
|
}
|
|
223
218
|
function measureTabs(view, cache, generation) {
|
|
224
|
-
const scale =
|
|
219
|
+
const scale = pageScaleAround(view.dom);
|
|
225
220
|
const defaultStopPt = documentDefaultTabStopPt(view.state);
|
|
226
221
|
const widths = new Map(tabWidths(view.state));
|
|
227
222
|
const paragraphs = /* @__PURE__ */ new Map();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A side story drawn as markup no editor view stands behind: a note the caret is not in, and later
|
|
3
|
+
* a comment body or a header preview.
|
|
4
|
+
*
|
|
5
|
+
* The runs are drawn by the same spec the editor's run view draws (`editor/views/runMarkView`), so
|
|
6
|
+
* a story set down here wraps where it would in an editor, and what one kind of story draws
|
|
7
|
+
* differently comes in as a node spec. Nothing here knows which kind of story it is drawing, or
|
|
8
|
+
* where the markup goes.
|
|
9
|
+
*
|
|
10
|
+
* The schema writes the document's own source onto what it draws (`w:pPr`, a run's `w:rPr`, a
|
|
11
|
+
* preserved fragment's XML) for an editor to read back, and nothing reads this markup back. So
|
|
12
|
+
* none of it is kept: a selection the browser copies out of a note, with no copy serializer of the
|
|
13
|
+
* editor behind it, then carries no more of the document than a copy out of the editor does
|
|
14
|
+
* (`schema/clipboard`).
|
|
15
|
+
*/
|
|
16
|
+
import { type DOMOutputSpec, type Node as PMNode } from "prosemirror-model";
|
|
17
|
+
import type { FontFallbacks } from "../../styles/fontStack";
|
|
18
|
+
/** How a node type is drawn in place of the way the schema draws it, by node type name */
|
|
19
|
+
export type StoryNodeSpecs = Readonly<Record<string, (node: PMNode) => DOMOutputSpec>>;
|
|
20
|
+
export interface StoryMarkupOptions {
|
|
21
|
+
readonly fontFallbacks: FontFallbacks;
|
|
22
|
+
readonly nodeSpecs?: StoryNodeSpecs;
|
|
23
|
+
}
|
|
24
|
+
export declare function storyMarkup(story: PMNode, { fontFallbacks, nodeSpecs }: StoryMarkupOptions): DocumentFragment;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/editor/stories/storyMarkup.ts
|
|
2
|
+
import {
|
|
3
|
+
DOMSerializer
|
|
4
|
+
} from "prosemirror-model";
|
|
5
|
+
import { runMarkSpec } from "../../schema/index.js";
|
|
6
|
+
var STYLED_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
7
|
+
"data-display",
|
|
8
|
+
"data-underline"
|
|
9
|
+
]);
|
|
10
|
+
function withoutSource(markup) {
|
|
11
|
+
markup.querySelectorAll("*").forEach((element) => {
|
|
12
|
+
for (const name of element.getAttributeNames()) {
|
|
13
|
+
if (name.startsWith("data-") && !STYLED_ATTRIBUTES.has(name)) {
|
|
14
|
+
element.removeAttribute(name);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return markup;
|
|
19
|
+
}
|
|
20
|
+
function storyMarkup(story, { fontFallbacks, nodeSpecs = {} }) {
|
|
21
|
+
const schema = story.type.schema;
|
|
22
|
+
const serializer = new DOMSerializer(
|
|
23
|
+
{ ...DOMSerializer.nodesFromSchema(schema), ...nodeSpecs },
|
|
24
|
+
{
|
|
25
|
+
...DOMSerializer.marksFromSchema(schema),
|
|
26
|
+
run: (mark) => runMarkSpec(mark.attrs, fontFallbacks)
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
const markup = document.createDocumentFragment();
|
|
30
|
+
serializer.serializeFragment(story.content, {}, markup);
|
|
31
|
+
return withoutSource(markup);
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
storyMarkup
|
|
35
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state one side story is edited in.
|
|
3
|
+
*
|
|
4
|
+
* A story is a document of the same schema as the body, so it is edited by the same plugins: the
|
|
5
|
+
* guards, the clipboard, the tabs, the display derivation. What it is not is a document of its
|
|
6
|
+
* own, so the ones that answer for a whole document stay out - the history, which the main
|
|
7
|
+
* document keeps for both (`./storyView`), the note and comment plugins, whose subjects a story
|
|
8
|
+
* does not hold, and the page decorations, since a story is drawn wherever its host puts it.
|
|
9
|
+
*
|
|
10
|
+
* The snapshot handed in is the main document's, which is what makes a run resolve here as it
|
|
11
|
+
* would in the body. Its caller narrows the two values a story answers differently: a story has
|
|
12
|
+
* nowhere to write a list definition, and the paper it wraps at is the one its anchor stands on.
|
|
13
|
+
*/
|
|
14
|
+
import type { Node as PMNode } from "prosemirror-model";
|
|
15
|
+
import { type Command, EditorState, type Plugin } from "prosemirror-state";
|
|
16
|
+
import type { EditingProtection } from "../../schema/protection";
|
|
17
|
+
import type { SliceNormalizer } from "../clipboard/normalizers";
|
|
18
|
+
import { type EditorDocument } from "../editorDocument";
|
|
19
|
+
import type { SurfaceCapabilities } from "./storyView";
|
|
20
|
+
export interface StoryStateOptions {
|
|
21
|
+
/** The story as the main document holds it */
|
|
22
|
+
readonly story: PMNode;
|
|
23
|
+
/** The main document's snapshot, which the story's display values are resolved against */
|
|
24
|
+
readonly document: EditorDocument;
|
|
25
|
+
readonly protection: EditingProtection;
|
|
26
|
+
/** The keys the surface binds ahead of the editor's own: undo, redo and Escape */
|
|
27
|
+
readonly keys: Record<string, Command>;
|
|
28
|
+
/** What the kind of story adds, which is asked before the editor's own keymaps */
|
|
29
|
+
readonly plugins: readonly Plugin[];
|
|
30
|
+
readonly normalizers: readonly SliceNormalizer[];
|
|
31
|
+
/** What this kind of story takes, which the editor's own keys are bound from */
|
|
32
|
+
readonly takes: SurfaceCapabilities;
|
|
33
|
+
}
|
|
34
|
+
export declare function storyEditorState({ story, document, protection, keys, plugins, normalizers, takes, }: StoryStateOptions): EditorState;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/editor/stories/storyState.ts
|
|
2
|
+
import { baseKeymap } from "prosemirror-commands";
|
|
3
|
+
import { dropCursor } from "prosemirror-dropcursor";
|
|
4
|
+
import { keymap } from "prosemirror-keymap";
|
|
5
|
+
import { EditorState } from "prosemirror-state";
|
|
6
|
+
import { tableEditing } from "prosemirror-tables";
|
|
7
|
+
import { docxClipboard } from "../clipboard/plugin.js";
|
|
8
|
+
import { editorDocument } from "../editorDocument.js";
|
|
9
|
+
import { compositionSelection } from "../plugins/compositionSelection.js";
|
|
10
|
+
import {
|
|
11
|
+
displayDerivation,
|
|
12
|
+
withDerivedDisplay
|
|
13
|
+
} from "../plugins/displayDerivation.js";
|
|
14
|
+
import { documentProtection } from "../plugins/documentProtection.js";
|
|
15
|
+
import { docxKeymap, NOTE_KEYS } from "../plugins/keymap.js";
|
|
16
|
+
import { lockedContent } from "../plugins/lockedContent.js";
|
|
17
|
+
import { numberingMarkers } from "../plugins/numberingDecorations.js";
|
|
18
|
+
import { tabCaret } from "../plugins/tabCaret.js";
|
|
19
|
+
import { tabDecorations } from "../plugins/tabDecorations.js";
|
|
20
|
+
import { tabLayout } from "../plugins/tabLayout.js";
|
|
21
|
+
import { tabPointer } from "../plugins/tabPointer.js";
|
|
22
|
+
var CAPABILITY_KEYS = {
|
|
23
|
+
"Mod-k": "link",
|
|
24
|
+
[NOTE_KEYS.footnote]: "note",
|
|
25
|
+
[NOTE_KEYS.endnote]: "note"
|
|
26
|
+
};
|
|
27
|
+
var KEYS_NO_STORY_TAKES = ["Mod-Enter"];
|
|
28
|
+
function storyKeymap(takes) {
|
|
29
|
+
return Object.fromEntries(
|
|
30
|
+
Object.entries(docxKeymap).filter(([key]) => {
|
|
31
|
+
if (KEYS_NO_STORY_TAKES.includes(key)) return false;
|
|
32
|
+
const needed = CAPABILITY_KEYS[key];
|
|
33
|
+
return needed === void 0 || takes.has(needed);
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
function storyEditorState({
|
|
38
|
+
story,
|
|
39
|
+
document,
|
|
40
|
+
protection,
|
|
41
|
+
keys,
|
|
42
|
+
plugins,
|
|
43
|
+
normalizers,
|
|
44
|
+
takes
|
|
45
|
+
}) {
|
|
46
|
+
return EditorState.create({
|
|
47
|
+
doc: withDerivedDisplay(story, document),
|
|
48
|
+
plugins: [
|
|
49
|
+
editorDocument(document),
|
|
50
|
+
lockedContent(),
|
|
51
|
+
documentProtection({ protection, author: null, editableComments: "own" }),
|
|
52
|
+
compositionSelection(),
|
|
53
|
+
// A kind's own key rule stands ahead of the keymaps below: the base keymap answers
|
|
54
|
+
// Backspace and Enter for every document, so a rule about an empty story would never be
|
|
55
|
+
// asked behind it
|
|
56
|
+
...plugins,
|
|
57
|
+
keymap(keys),
|
|
58
|
+
keymap(storyKeymap(takes)),
|
|
59
|
+
keymap(baseKeymap),
|
|
60
|
+
dropCursor(),
|
|
61
|
+
docxClipboard({ normalizers }),
|
|
62
|
+
tableEditing(),
|
|
63
|
+
tabDecorations(),
|
|
64
|
+
tabPointer(),
|
|
65
|
+
tabLayout(),
|
|
66
|
+
tabCaret(),
|
|
67
|
+
displayDerivation(),
|
|
68
|
+
numberingMarkers()
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
storyEditorState
|
|
74
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one editing view a side story is edited in, mounted over the story the caret went into and
|
|
3
|
+
* taken down when it leaves.
|
|
4
|
+
*
|
|
5
|
+
* Everything typed here leaves through the host, which writes it into the main document as one
|
|
6
|
+
* story change. That is what keeps the guards, the locks and the protection judging a story edit
|
|
7
|
+
* where they judge a body edit, and what leaves the main document holding the only history: the
|
|
8
|
+
* undo keys here run the host's.
|
|
9
|
+
*
|
|
10
|
+
* This file does not know which kind of story it is drawing or where it is drawn: a note stands
|
|
11
|
+
* at the foot of a page, a comment body in a rail beside it, a header in a page margin. What
|
|
12
|
+
* differs between the kinds arrives as `StoryExtensions`.
|
|
13
|
+
*/
|
|
14
|
+
import type { Node as PMNode } from "prosemirror-model";
|
|
15
|
+
import { type EditorState, type Plugin } from "prosemirror-state";
|
|
16
|
+
import { EditorView, type NodeViewConstructor } from "prosemirror-view";
|
|
17
|
+
import { type StoryKey } from "../../schema/stories";
|
|
18
|
+
import type { FontFallbacks } from "../../styles/fontStack";
|
|
19
|
+
import type { SliceNormalizer } from "../clipboard/normalizers";
|
|
20
|
+
import type { EditorDocument } from "../editorDocument";
|
|
21
|
+
export type NodeViewMap = Record<string, NodeViewConstructor>;
|
|
22
|
+
/** What every surface is read and dispatched through, whichever of the two it is */
|
|
23
|
+
interface SurfaceContents {
|
|
24
|
+
readonly view: EditorView;
|
|
25
|
+
/** The state on screen, which a control drawn from the surface re-decides itself from */
|
|
26
|
+
readonly state: EditorState;
|
|
27
|
+
readonly takes: SurfaceCapabilities;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The surface holding the caret, which is either the body or one story with the view over it.
|
|
31
|
+
*
|
|
32
|
+
* Two editing views cannot stand at once, and this is where that rule is written down: a caller
|
|
33
|
+
* holding this value has no way to name a second one. What holds it true is the state that decides
|
|
34
|
+
* which story is open (`ui/notes/useStorySurface`).
|
|
35
|
+
*/
|
|
36
|
+
export type ActiveSurface = ({
|
|
37
|
+
readonly surface: "body";
|
|
38
|
+
} & SurfaceContents) | ({
|
|
39
|
+
readonly surface: "story";
|
|
40
|
+
readonly key: StoryKey;
|
|
41
|
+
} & SurfaceContents);
|
|
42
|
+
export interface StoryHost {
|
|
43
|
+
/** The main state the story is read from and judged against */
|
|
44
|
+
readonly state: () => EditorState;
|
|
45
|
+
/** Writes the story into the main document; false when the main document refused it */
|
|
46
|
+
write(key: StoryKey, story: PMNode): boolean;
|
|
47
|
+
undo(): boolean;
|
|
48
|
+
redo(): boolean;
|
|
49
|
+
/** Hands the caret back to the main document (Escape) */
|
|
50
|
+
leave(key: StoryKey): void;
|
|
51
|
+
/** Whether edits to this story are closed where it is anchored */
|
|
52
|
+
shut(key: StoryKey): boolean;
|
|
53
|
+
/** Registers the view that holds the caret, null when it lets go */
|
|
54
|
+
activate(key: StoryKey, view: EditorView | null): void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What a surface takes beyond character and paragraph formatting.
|
|
58
|
+
*
|
|
59
|
+
* Every one of these puts something into the document that the part the surface is written back as
|
|
60
|
+
* has to carry - a relationship, a definition, a part of its own - so which of them a surface takes
|
|
61
|
+
* is the surface's own to declare, and a control or a key that puts one in asks here rather than
|
|
62
|
+
* asking which kind of surface it is.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SURFACE_CAPABILITIES: readonly ["list", "table", "image", "link", "note", "comment"];
|
|
65
|
+
export type SurfaceCapability = (typeof SURFACE_CAPABILITIES)[number];
|
|
66
|
+
export type SurfaceCapabilities = ReadonlySet<SurfaceCapability>;
|
|
67
|
+
/** What the body takes, which is everything: it is written back as the document part itself */
|
|
68
|
+
export declare const EVERY_CAPABILITY: SurfaceCapabilities;
|
|
69
|
+
/** What a surface whose writer carries none of them takes */
|
|
70
|
+
export declare const NO_CAPABILITY: SurfaceCapabilities;
|
|
71
|
+
export interface StoryExtensions {
|
|
72
|
+
readonly plugins: readonly Plugin[];
|
|
73
|
+
readonly nodeViews: NodeViewMap;
|
|
74
|
+
readonly normalizers: readonly SliceNormalizer[];
|
|
75
|
+
/** What this kind of story takes, which its keys and the controls over it are drawn from */
|
|
76
|
+
readonly takes: SurfaceCapabilities;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Where the caret goes when the view is mounted: back where it stood, or at the point the reader
|
|
80
|
+
* pressed, which is what a press on a story no view stands over yet has to say.
|
|
81
|
+
*/
|
|
82
|
+
export type StoryCaret = {
|
|
83
|
+
readonly kind: "at";
|
|
84
|
+
readonly anchor: number;
|
|
85
|
+
readonly head: number;
|
|
86
|
+
} | {
|
|
87
|
+
readonly kind: "point";
|
|
88
|
+
readonly left: number;
|
|
89
|
+
readonly top: number;
|
|
90
|
+
};
|
|
91
|
+
export interface StoryViewOptions {
|
|
92
|
+
readonly mount: HTMLElement;
|
|
93
|
+
readonly host: StoryHost;
|
|
94
|
+
readonly key: StoryKey;
|
|
95
|
+
readonly document: EditorDocument;
|
|
96
|
+
readonly fontFallbacks: FontFallbacks;
|
|
97
|
+
readonly extensions: StoryExtensions;
|
|
98
|
+
/** Where the caret opens. The end of the story when none is given */
|
|
99
|
+
readonly caret?: StoryCaret | null;
|
|
100
|
+
/**
|
|
101
|
+
* Called with every state this view goes to, which is what a control drawn from the story - a
|
|
102
|
+
* toolbar acting on it - re-decides itself from.
|
|
103
|
+
*/
|
|
104
|
+
readonly onStateChange?: (state: EditorState) => void;
|
|
105
|
+
}
|
|
106
|
+
export interface StoryView {
|
|
107
|
+
readonly view: EditorView;
|
|
108
|
+
/** Takes a changed main state in; replaces the content only when the story says something else */
|
|
109
|
+
sync(): void;
|
|
110
|
+
destroy(): void;
|
|
111
|
+
}
|
|
112
|
+
/** Where the caret stands, in the shape a view mounted again over the same story opens at */
|
|
113
|
+
export declare function caretOf(state: EditorState): StoryCaret;
|
|
114
|
+
export declare function createStoryView({ mount, host, key, document, fontFallbacks, extensions, caret, onStateChange, }: StoryViewOptions): StoryView;
|
|
115
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/editor/stories/storyView.ts
|
|
2
|
+
import {
|
|
3
|
+
TextSelection
|
|
4
|
+
} from "prosemirror-state";
|
|
5
|
+
import { EditorView } from "prosemirror-view";
|
|
6
|
+
import { editingProtection, editsShut } from "../../schema/protectionState.js";
|
|
7
|
+
import { sameStory, storyNodeOf } from "../../schema/stories.js";
|
|
8
|
+
import { editorClassNames } from "../../styles/classNames.js";
|
|
9
|
+
import { runMarkView } from "../views/runMarkView.js";
|
|
10
|
+
import { storyEditorState } from "./storyState.js";
|
|
11
|
+
var SURFACE_CAPABILITIES = [
|
|
12
|
+
"list",
|
|
13
|
+
"table",
|
|
14
|
+
"image",
|
|
15
|
+
"link",
|
|
16
|
+
"note",
|
|
17
|
+
"comment"
|
|
18
|
+
];
|
|
19
|
+
var EVERY_CAPABILITY = new Set(
|
|
20
|
+
SURFACE_CAPABILITIES
|
|
21
|
+
);
|
|
22
|
+
var NO_CAPABILITY = /* @__PURE__ */ new Set();
|
|
23
|
+
function caretOf(state) {
|
|
24
|
+
return {
|
|
25
|
+
kind: "at",
|
|
26
|
+
anchor: state.selection.anchor,
|
|
27
|
+
head: state.selection.head
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function placed(state, at, to) {
|
|
31
|
+
const size = state.doc.content.size;
|
|
32
|
+
const $anchor = state.doc.resolve(Math.min(Math.max(at, 0), size));
|
|
33
|
+
const $head = state.doc.resolve(Math.min(Math.max(to, 0), size));
|
|
34
|
+
return state.apply(
|
|
35
|
+
state.tr.setSelection(TextSelection.between($anchor, $head))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
function endComposition(view) {
|
|
39
|
+
if (!view.composing) return;
|
|
40
|
+
const marks = view.state.storedMarks ?? view.state.selection.$from.marks();
|
|
41
|
+
view.dispatch(
|
|
42
|
+
view.state.tr.setStoredMarks(marks).setMeta("addToHistory", false)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function createStoryView({
|
|
46
|
+
mount,
|
|
47
|
+
host,
|
|
48
|
+
key,
|
|
49
|
+
document,
|
|
50
|
+
fontFallbacks,
|
|
51
|
+
extensions,
|
|
52
|
+
caret = null,
|
|
53
|
+
onStateChange
|
|
54
|
+
}) {
|
|
55
|
+
const standing = () => host.shut(key) ? "readOnly" : editingProtection(host.state());
|
|
56
|
+
let protection = standing();
|
|
57
|
+
const keys = {
|
|
58
|
+
"Mod-z": () => host.undo(),
|
|
59
|
+
"Mod-y": () => host.redo(),
|
|
60
|
+
"Shift-Mod-z": () => host.redo(),
|
|
61
|
+
Escape: () => {
|
|
62
|
+
host.leave(key);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const built = (story) => storyEditorState({
|
|
67
|
+
story,
|
|
68
|
+
document,
|
|
69
|
+
protection,
|
|
70
|
+
keys,
|
|
71
|
+
plugins: extensions.plugins,
|
|
72
|
+
normalizers: extensions.normalizers,
|
|
73
|
+
takes: extensions.takes
|
|
74
|
+
});
|
|
75
|
+
const storyNow = () => storyNodeOf(host.state().doc, key);
|
|
76
|
+
const opening = storyNow();
|
|
77
|
+
if (opening === null) {
|
|
78
|
+
throw new Error(`the document holds no story named ${key}`);
|
|
79
|
+
}
|
|
80
|
+
const view = new EditorView(mount, {
|
|
81
|
+
state: built(opening),
|
|
82
|
+
// A standing that shuts the story shuts typing in it, and is read off the state so that a
|
|
83
|
+
// mode switched while the story is open takes effect without a new view
|
|
84
|
+
editable: (current) => !editsShut(current),
|
|
85
|
+
attributes: { class: editorClassNames.storyBody },
|
|
86
|
+
// The schema can only draw a run with the default fallbacks; this view draws the host's
|
|
87
|
+
markViews: { run: runMarkView(fontFallbacks) },
|
|
88
|
+
nodeViews: extensions.nodeViews,
|
|
89
|
+
dispatchTransaction(transaction) {
|
|
90
|
+
const next = view.state.applyTransaction(transaction);
|
|
91
|
+
if (next.state.doc === view.state.doc) {
|
|
92
|
+
show(next.state);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const before = view.state;
|
|
96
|
+
show(next.state);
|
|
97
|
+
if (host.write(key, next.state.doc)) return;
|
|
98
|
+
if (sameStory(storyNow(), next.state.doc)) return;
|
|
99
|
+
show(before);
|
|
100
|
+
endComposition(view);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const show = (next) => {
|
|
104
|
+
view.updateState(next);
|
|
105
|
+
onStateChange?.(next);
|
|
106
|
+
};
|
|
107
|
+
const open = caret ?? null;
|
|
108
|
+
if (open === null) {
|
|
109
|
+
const end = view.state.doc.content.size;
|
|
110
|
+
show(placed(view.state, end, end));
|
|
111
|
+
} else if (open.kind === "at") {
|
|
112
|
+
show(placed(view.state, open.anchor, open.head));
|
|
113
|
+
} else {
|
|
114
|
+
const at = view.posAtCoords({ left: open.left, top: open.top });
|
|
115
|
+
const pos = at?.pos ?? view.state.doc.content.size;
|
|
116
|
+
show(placed(view.state, pos, pos));
|
|
117
|
+
}
|
|
118
|
+
const registerFocus = () => host.activate(key, view);
|
|
119
|
+
view.dom.addEventListener("focus", registerFocus);
|
|
120
|
+
return {
|
|
121
|
+
view,
|
|
122
|
+
sync() {
|
|
123
|
+
const story = storyNow();
|
|
124
|
+
if (story === null) return;
|
|
125
|
+
const wanted = standing();
|
|
126
|
+
if (wanted === protection && sameStory(story, view.state.doc)) return;
|
|
127
|
+
const { anchor, head } = view.state.selection;
|
|
128
|
+
protection = wanted;
|
|
129
|
+
show(placed(built(story), anchor, head));
|
|
130
|
+
},
|
|
131
|
+
destroy() {
|
|
132
|
+
view.dom.removeEventListener("focus", registerFocus);
|
|
133
|
+
host.activate(key, null);
|
|
134
|
+
view.destroy();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
EVERY_CAPABILITY,
|
|
140
|
+
NO_CAPABILITY,
|
|
141
|
+
SURFACE_CAPABILITIES,
|
|
142
|
+
caretOf,
|
|
143
|
+
createStoryView
|
|
144
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -16,12 +16,12 @@ export type { DocxEditorHandle, DocxEditorMode, DocxEditorProps, } from "./DocxE
|
|
|
16
16
|
export { DocxEditor } from "./DocxEditor";
|
|
17
17
|
export type { DocxBytes, DocxSource } from "./docx/importDocx";
|
|
18
18
|
/** What the handle and a `blocked` download report: each reason the document cannot be written, under the code `DocxExportError` would throw it with */
|
|
19
|
-
export type { ExportProblem } from "./docx/invariants";
|
|
19
|
+
export type { ExportPartName, ExportProblem, ExportProblemReason, ExportProblemStory, ExportStoryKind, } from "./docx/invariants";
|
|
20
20
|
export type { DownloadDocxOptions, DownloadDocxResult, } from "./download";
|
|
21
21
|
export { downloadDocx } from "./download";
|
|
22
22
|
export type { CommentAuthor } from "./editor/commands/commentCommands";
|
|
23
23
|
export type { CellFormat, CellVerticalAlign, DocumentDefaults, HighlightName, LineSpacing, NumberingRef, ParagraphAlign, ParagraphFormat, RowFormat, RowHeight, RunFormat, TableFormat, TableWidth, TableWidthType, UnderlineKind, VerticalAlign, } from "./model/format";
|
|
24
|
-
export type { DocxExportErrorCode, DocxImportErrorCode, } from "./ooxml/errors";
|
|
24
|
+
export type { DocxExportErrorCode, DocxExportErrorOptions, DocxImportErrorCode, } from "./ooxml/errors";
|
|
25
25
|
export { DocxExportError, DocxImportError } from "./ooxml/errors";
|
|
26
26
|
export { docxSchema } from "./schema";
|
|
27
27
|
/** Whose comments the panel offers to edit, which `DocxEditorMode` takes */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* because it has a speller here, so a marker can never be counted in a format nothing can write.
|
|
7
7
|
*/
|
|
8
8
|
/** A `w:numFmt` this editor spells out. Every other one is drawn as a decimal */
|
|
9
|
-
export type NumberFormat = "decimal" | "decimalZero" | "bullet" | "lowerLetter" | "upperLetter" | "lowerRoman" | "upperRoman" | "ganada" | "koreanDigital" | "chineseCounting";
|
|
9
|
+
export type NumberFormat = "decimal" | "decimalZero" | "bullet" | "lowerLetter" | "upperLetter" | "lowerRoman" | "upperRoman" | "ganada" | "koreanDigital" | "chineseCounting" | "chicago";
|
|
10
10
|
export declare function isNumberFormat(value: string | null): value is NumberFormat;
|
|
11
11
|
/**
|
|
12
12
|
* The count as this format writes it, and as a decimal where spelling it out would run past the
|
|
@@ -31,6 +31,7 @@ function repeated(symbols, count) {
|
|
|
31
31
|
return (symbols[index] ?? "").repeat(times);
|
|
32
32
|
}
|
|
33
33
|
var LATIN = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
34
|
+
var CHICAGO = "*\u2020\u2021\xA7";
|
|
34
35
|
var GANADA = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
|
|
35
36
|
var KOREAN_DIGITS = "\uC601\uC77C\uC774\uC0BC\uC0AC\uC624\uC721\uCE60\uD314\uAD6C";
|
|
36
37
|
function digits(symbols, count) {
|
|
@@ -80,7 +81,11 @@ var NUMBER_SPELLERS = {
|
|
|
80
81
|
spell: (count) => digits(KOREAN_DIGITS, count),
|
|
81
82
|
countsPerChar: POSITIONAL
|
|
82
83
|
},
|
|
83
|
-
chineseCounting: { spell: chineseCounting, countsPerChar: POSITIONAL }
|
|
84
|
+
chineseCounting: { spell: chineseCounting, countsPerChar: POSITIONAL },
|
|
85
|
+
chicago: {
|
|
86
|
+
spell: (count) => repeated(CHICAGO, count),
|
|
87
|
+
countsPerChar: CHICAGO.length
|
|
88
|
+
}
|
|
84
89
|
};
|
|
85
90
|
function isNumberFormat(value) {
|
|
86
91
|
return value !== null && Object.hasOwn(NUMBER_SPELLERS, value);
|