@stll/folio-core 0.54.1 → 0.54.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/dist/ai-edits/apply.d.ts +1 -1
- package/dist/ai-edits/apply.js +18 -31
- package/dist/ai-edits/headless.d.ts +1 -2
- package/dist/ai-edits/headless.js +12 -27
- package/dist/ai-edits/read.d.ts +1 -0
- package/dist/ai-edits/read.js +7 -2
- package/dist/ai-edits/table-mutation-plan.d.ts +2 -1
- package/dist/ai-edits/table-mutation-plan.js +39 -1
- package/dist/ai-suggestions/apply.d.ts +1 -1
- package/dist/ai-suggestions/apply.js +3 -2
- package/dist/compare/compare.js +2 -1
- package/dist/docx/bookmarkIds.d.ts +11 -0
- package/dist/docx/bookmarkIds.js +33 -0
- package/dist/docx/documentParser.d.ts +14 -1
- package/dist/docx/documentParser.js +17 -4
- package/dist/docx/drawingIdNormalization.js +23 -8
- package/dist/docx/ensureParaIds.d.ts +5 -0
- package/dist/docx/ensureParaIds.js +10 -2
- package/dist/docx/listNumberingInstances.js +11 -9
- package/dist/docx/noteIds.d.ts +4 -0
- package/dist/docx/noteIds.js +13 -0
- package/dist/docx/numberingIds.d.ts +14 -0
- package/dist/docx/numberingIds.js +16 -0
- package/dist/docx/numericIdAllocator.d.ts +12 -0
- package/dist/docx/numericIdAllocator.js +33 -0
- package/dist/docx/numericIdNormalization.d.ts +16 -0
- package/dist/docx/numericIdNormalization.js +174 -0
- package/dist/docx/paragraphTextBoxEnrichment.js +2 -1
- package/dist/docx/parser.js +17 -5
- package/dist/docx/rasterMime.d.ts +64 -0
- package/dist/docx/rasterMime.js +116 -0
- package/dist/docx/replyToComment.js +13 -6
- package/dist/docx/rezip.js +14 -7
- package/dist/docx/selectiveSave.js +10 -0
- package/dist/docx/serializer/partNamespaces.js +4 -2
- package/dist/docx/serializer/runSerializer.js +5 -4
- package/dist/docx/serializer/trackedChangeAttributes.js +7 -1
- package/dist/docx/server/build.js +14 -3
- package/dist/docx/server/createBilingualDocument.js +10 -9
- package/dist/docx/streamingXmlParser.d.ts +10 -1
- package/dist/docx/streamingXmlParser.js +88 -19
- package/dist/docx/unzip.d.ts +3 -1
- package/dist/docx/unzip.js +36 -16
- package/dist/layout-bridge/convert/fixedTableColumnWidths.d.ts +6 -0
- package/dist/layout-bridge/convert/fixedTableColumnWidths.js +23 -0
- package/dist/layout-bridge/convert/tableConversion.js +4 -1
- package/dist/layout-painter/index.js +2 -5
- package/dist/paged-layout/editorScrollRoot.d.ts +15 -0
- package/dist/paged-layout/editorScrollRoot.js +72 -0
- package/dist/paged-layout/scrollToPmPosition.js +8 -22
- package/dist/prosemirror/commands/clearParagraphIndent.d.ts +6 -0
- package/dist/prosemirror/commands/clearParagraphIndent.js +25 -0
- package/dist/prosemirror/commentIdAllocator.d.ts +15 -22
- package/dist/prosemirror/commentIdAllocator.js +50 -25
- package/dist/prosemirror/extensions/core/HistoryExtension.js +97 -5
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -1
- package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +22 -28
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +8 -6
- package/dist/prosemirror/plugins/revisionIds.d.ts +23 -19
- package/dist/prosemirror/plugins/revisionIds.js +110 -29
- package/dist/prosemirror/plugins/suggestionMode.d.ts +2 -2
- package/dist/prosemirror/plugins/suggestionMode.js +196 -40
- package/dist/prosemirror/storyListNumbering.js +11 -9
- package/dist/prosemirror/textInput.d.ts +6 -1
- package/dist/prosemirror/textInput.js +9 -2
- package/dist/prosemirror/utils/visualLineNavigation.js +6 -8
- package/dist/utils/mergeDocumentContent.d.ts +2 -2
- package/dist/utils/mergeDocumentContent.js +11 -8
- package/package.json +2 -2
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { expectTableCellAttrs, expectTableRowAttrs } from "../../prosemirror/attrs/index.js";
|
|
2
|
+
//#region src/layout-bridge/convert/fixedTableColumnWidths.ts
|
|
3
|
+
/** Resolve absolute cell preferences for an unbounded fixed-layout grid (§17.4.53). */
|
|
4
|
+
const fixedTableColumnWidths = (table, grid) => {
|
|
5
|
+
const preferred = /* @__PURE__ */ new Map();
|
|
6
|
+
for (let rowIndex = 0; rowIndex < table.childCount; rowIndex++) {
|
|
7
|
+
const row = table.child(rowIndex);
|
|
8
|
+
const rowAttrs = expectTableRowAttrs(row);
|
|
9
|
+
if (rowAttrs.trIns || rowAttrs.trDel || rowAttrs.hidden) return grid;
|
|
10
|
+
let column = Math.max(0, rowAttrs._originalFormatting?.gridBefore ?? 0);
|
|
11
|
+
for (let cellIndex = 0; cellIndex < row.childCount; cellIndex++) {
|
|
12
|
+
const attrs = expectTableCellAttrs(row.child(cellIndex));
|
|
13
|
+
if (attrs._omittedGridSlot) continue;
|
|
14
|
+
const width = attrs._authoredWidth;
|
|
15
|
+
if (attrs.colspan !== 1 || attrs.rowspan !== 1 || attrs._preserveVMergeRestart || attrs._originalFormatting?.vMerge !== void 0 || !width || width.type !== "dxa" || !Number.isFinite(width.value) || width.value <= 0) return grid;
|
|
16
|
+
if (column < grid.length) preferred.set(column, Math.max(preferred.get(column) ?? 0, width.value));
|
|
17
|
+
column++;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return grid.map((width, column) => preferred.get(column) ?? width);
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { fixedTableColumnWidths };
|
|
@@ -3,6 +3,7 @@ import { DEFAULT_TEXTBOX_MARGINS } from "../../layout-engine/types.js";
|
|
|
3
3
|
import { expectParagraphAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs } from "../../prosemirror/attrs/index.js";
|
|
4
4
|
import { textBoxHostParagraph } from "../../prosemirror/textBoxHostParagraph.js";
|
|
5
5
|
import { normalizeShapeTextAnchor } from "../../types/documentEnumValues.js";
|
|
6
|
+
import { fixedTableColumnWidths } from "./fixedTableColumnWidths.js";
|
|
6
7
|
import { convertBorderSpecToLayout } from "./flowBorders.js";
|
|
7
8
|
import { nextBlockId, twipsToPixels } from "./flowConversionShared.js";
|
|
8
9
|
import { countPageBreakRuns, hasSingleLeadingProjectedPageBreak } from "./pageBreakSplitting.js";
|
|
@@ -183,7 +184,9 @@ function convertTable(node, startPos, options) {
|
|
|
183
184
|
if (child.type.name === "tableRow") rows.push(convertTableRow(child, offset, options, rowContext));
|
|
184
185
|
offset += child.nodeSize;
|
|
185
186
|
});
|
|
186
|
-
|
|
187
|
+
const columnWidthsTwips = attrs.columnWidths;
|
|
188
|
+
const hasTableWidth = (attrs.widthType === "dxa" || attrs.widthType === "pct") && (attrs.width ?? 0) > 0;
|
|
189
|
+
let columnWidths = (columnWidthsTwips && attrs._originalFormatting?.layout === "fixed" && !hasTableWidth ? fixedTableColumnWidths(node, columnWidthsTwips) : columnWidthsTwips)?.map(twipsToPixels);
|
|
187
190
|
const width = attrs.width;
|
|
188
191
|
const widthType = attrs.widthType;
|
|
189
192
|
if (!columnWidths && rows.length > 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { scrollEditorElementIntoView } from "../paged-layout/editorScrollRoot.js";
|
|
2
2
|
import { createDefaultRegistry } from "./registry/modules.js";
|
|
3
3
|
import { createFeatureRegistry } from "./registry/registry.js";
|
|
4
4
|
import { FRAGMENT_CLASS_NAMES, renderFragment } from "./renderFragment.js";
|
|
@@ -167,10 +167,7 @@ var LayoutPainter = class {
|
|
|
167
167
|
*/
|
|
168
168
|
scrollToPage(pageNumber) {
|
|
169
169
|
const state = this.pageStates.find((s) => s.pageNumber === pageNumber);
|
|
170
|
-
if (state?.element) state.element
|
|
171
|
-
behavior: prefersReducedMotionBehavior(),
|
|
172
|
-
block: "start"
|
|
173
|
-
});
|
|
170
|
+
if (state?.element) scrollEditorElementIntoView(state.element, { block: "start" });
|
|
174
171
|
}
|
|
175
172
|
};
|
|
176
173
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/paged-layout/editorScrollRoot.d.ts
|
|
2
|
+
declare const getEditorScrollRoot: (element: HTMLElement | null) => HTMLElement | null;
|
|
3
|
+
/** Resolve the root even when a caller holds a painted descendant or viewport. */
|
|
4
|
+
declare const scrollEditorTo: (element: HTMLElement | null, options: ScrollToOptions) => void;
|
|
5
|
+
declare const scrollEditorBy: (element: HTMLElement | null, delta: number) => void;
|
|
6
|
+
type ScrollEditorElementOptions = {
|
|
7
|
+
block?: "start" | "center" | "end" | "nearest";
|
|
8
|
+
behavior?: ScrollBehavior;
|
|
9
|
+
/** Space to leave between the target and the visible root edge. */
|
|
10
|
+
margin?: number;
|
|
11
|
+
};
|
|
12
|
+
/** Scroll only the marked root, including when the target is inside a scaled viewport. */
|
|
13
|
+
declare const scrollEditorElementIntoView: (element: HTMLElement | null, { block, behavior, margin }?: ScrollEditorElementOptions) => void;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { getEditorScrollRoot, scrollEditorBy, scrollEditorElementIntoView, scrollEditorTo };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { prefersReducedMotionBehavior } from "./scrollNavigation.js";
|
|
2
|
+
//#region src/paged-layout/editorScrollRoot.ts
|
|
3
|
+
/** The marked editor owns scrolling; navigation must never move host ancestors. */
|
|
4
|
+
const EDITOR_SCROLL_ROOT_SELECTOR = "[data-folio-scroll]";
|
|
5
|
+
const getEditorScrollRoot = (element) => element?.closest(EDITOR_SCROLL_ROOT_SELECTOR) ?? null;
|
|
6
|
+
/** Resolve the root even when a caller holds a painted descendant or viewport. */
|
|
7
|
+
const scrollEditorTo = (element, options) => {
|
|
8
|
+
getEditorScrollRoot(element)?.scrollTo(options);
|
|
9
|
+
};
|
|
10
|
+
const scrollEditorBy = (element, delta) => {
|
|
11
|
+
const root = getEditorScrollRoot(element);
|
|
12
|
+
if (root) scrollEditorTo(root, {
|
|
13
|
+
top: root.scrollTop + delta,
|
|
14
|
+
behavior: "instant"
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
const nearestScrollOffset = ({ targetStart, targetEnd, viewportStart, viewportEnd }) => {
|
|
18
|
+
const above = targetStart < viewportStart;
|
|
19
|
+
const below = targetEnd > viewportEnd;
|
|
20
|
+
if (above && below) return 0;
|
|
21
|
+
const targetSize = targetEnd - targetStart;
|
|
22
|
+
const viewportSize = viewportEnd - viewportStart;
|
|
23
|
+
if (above && targetSize <= viewportSize || below && targetSize > viewportSize) return targetStart - viewportStart;
|
|
24
|
+
if (below && targetSize <= viewportSize || above && targetSize > viewportSize) return targetEnd - viewportEnd;
|
|
25
|
+
return 0;
|
|
26
|
+
};
|
|
27
|
+
/** Scroll only the marked root, including when the target is inside a scaled viewport. */
|
|
28
|
+
const scrollEditorElementIntoView = (element, { block = "center", behavior = prefersReducedMotionBehavior(), margin = 0 } = {}) => {
|
|
29
|
+
const root = getEditorScrollRoot(element);
|
|
30
|
+
if (!root || !element) return;
|
|
31
|
+
const targetRect = element.getBoundingClientRect();
|
|
32
|
+
const rootRect = root.getBoundingClientRect();
|
|
33
|
+
const visibleTop = rootRect.top + root.clientTop;
|
|
34
|
+
const targetTop = targetRect.top - visibleTop + root.scrollTop;
|
|
35
|
+
const targetBottom = targetRect.bottom - visibleTop + root.scrollTop;
|
|
36
|
+
let top;
|
|
37
|
+
switch (block) {
|
|
38
|
+
case "start":
|
|
39
|
+
top = targetTop - margin;
|
|
40
|
+
break;
|
|
41
|
+
case "center":
|
|
42
|
+
top = targetTop - (root.clientHeight - targetRect.height) / 2;
|
|
43
|
+
break;
|
|
44
|
+
case "end":
|
|
45
|
+
top = targetBottom - root.clientHeight + margin;
|
|
46
|
+
break;
|
|
47
|
+
case "nearest":
|
|
48
|
+
top = root.scrollTop + nearestScrollOffset({
|
|
49
|
+
targetStart: targetTop,
|
|
50
|
+
targetEnd: targetBottom,
|
|
51
|
+
viewportStart: root.scrollTop + margin,
|
|
52
|
+
viewportEnd: root.scrollTop + root.clientHeight - margin
|
|
53
|
+
});
|
|
54
|
+
break;
|
|
55
|
+
default: return block;
|
|
56
|
+
}
|
|
57
|
+
const visibleLeft = rootRect.left + root.clientLeft;
|
|
58
|
+
const left = root.scrollLeft + nearestScrollOffset({
|
|
59
|
+
targetStart: targetRect.left,
|
|
60
|
+
targetEnd: targetRect.right,
|
|
61
|
+
viewportStart: visibleLeft,
|
|
62
|
+
viewportEnd: visibleLeft + root.clientWidth
|
|
63
|
+
});
|
|
64
|
+
if (top === root.scrollTop && left === root.scrollLeft) return;
|
|
65
|
+
scrollEditorTo(root, {
|
|
66
|
+
top: Math.max(0, top),
|
|
67
|
+
left: Math.max(0, left),
|
|
68
|
+
behavior
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { getEditorScrollRoot, scrollEditorBy, scrollEditorElementIntoView, scrollEditorTo };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { findBodyPmAnchor, findBodyPmAnchors } from "../layout-bridge/dom/findBodyPmSpans.js";
|
|
2
2
|
import { findPageShellForPmPos } from "../layout-painter/renderPage.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getEditorScrollRoot, scrollEditorElementIntoView } from "./editorScrollRoot.js";
|
|
4
|
+
import { isValidPmScrollPosition } from "./scrollNavigation.js";
|
|
4
5
|
//#region src/paged-layout/scrollToPmPosition.ts
|
|
5
6
|
/** Class on the pages container `PagedEditor` paints into. */
|
|
6
7
|
const PAGES_CONTAINER_CLASS = "paged-editor__pages";
|
|
@@ -9,10 +10,7 @@ const scrollPagesToPmPosition = (pageContainer, pmPos) => {
|
|
|
9
10
|
if (!isValidPmScrollPosition(pmPos)) return;
|
|
10
11
|
const exact = findBodyPmAnchor(pageContainer, pmPos);
|
|
11
12
|
if (exact) {
|
|
12
|
-
exact
|
|
13
|
-
behavior: prefersReducedMotionBehavior(),
|
|
14
|
-
block: "center"
|
|
15
|
-
});
|
|
13
|
+
scrollEditorElementIntoView(exact);
|
|
16
14
|
return;
|
|
17
15
|
}
|
|
18
16
|
let runMatch = null;
|
|
@@ -27,28 +25,19 @@ const scrollPagesToPmPosition = (pageContainer, pmPos) => {
|
|
|
27
25
|
}
|
|
28
26
|
}
|
|
29
27
|
if (runMatch) {
|
|
30
|
-
runMatch
|
|
31
|
-
behavior: prefersReducedMotionBehavior(),
|
|
32
|
-
block: "center"
|
|
33
|
-
});
|
|
28
|
+
scrollEditorElementIntoView(runMatch);
|
|
34
29
|
return;
|
|
35
30
|
}
|
|
36
31
|
const shellHit = findPageShellForPmPos(pageContainer, pmPos);
|
|
37
32
|
if (!shellHit) return;
|
|
38
33
|
const { element: shell } = shellHit;
|
|
39
|
-
shell
|
|
40
|
-
behavior: prefersReducedMotionBehavior(),
|
|
41
|
-
block: "center"
|
|
42
|
-
});
|
|
34
|
+
scrollEditorElementIntoView(shell);
|
|
43
35
|
let attempts = 0;
|
|
44
36
|
const refine = () => {
|
|
45
37
|
attempts++;
|
|
46
38
|
const exactInShell = findBodyPmAnchor(shell, pmPos);
|
|
47
39
|
if (exactInShell) {
|
|
48
|
-
exactInShell
|
|
49
|
-
behavior: prefersReducedMotionBehavior(),
|
|
50
|
-
block: "center"
|
|
51
|
-
});
|
|
40
|
+
scrollEditorElementIntoView(exactInShell);
|
|
52
41
|
return;
|
|
53
42
|
}
|
|
54
43
|
let bestEl = null;
|
|
@@ -68,10 +57,7 @@ const scrollPagesToPmPosition = (pageContainer, pmPos) => {
|
|
|
68
57
|
}
|
|
69
58
|
}
|
|
70
59
|
if (bestEl) {
|
|
71
|
-
bestEl
|
|
72
|
-
behavior: prefersReducedMotionBehavior(),
|
|
73
|
-
block: "center"
|
|
74
|
-
});
|
|
60
|
+
scrollEditorElementIntoView(bestEl);
|
|
75
61
|
return;
|
|
76
62
|
}
|
|
77
63
|
if (attempts < 20) requestAnimationFrame(refine);
|
|
@@ -86,7 +72,7 @@ const scrollPagesToPmPosition = (pageContainer, pmPos) => {
|
|
|
86
72
|
* callers can fall back to coordinate-based scrolling.
|
|
87
73
|
*/
|
|
88
74
|
const scrollFolioPositionIntoView = (view, pmPos) => {
|
|
89
|
-
const pageContainer = view.dom
|
|
75
|
+
const pageContainer = getEditorScrollRoot(view.dom)?.querySelector(`.paged-editor__pages`) ?? null;
|
|
90
76
|
if (!pageContainer) return false;
|
|
91
77
|
scrollPagesToPmPosition(pageContainer, pmPos);
|
|
92
78
|
return true;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Command } from "prosemirror-state";
|
|
2
|
+
//#region src/prosemirror/commands/clearParagraphIndent.d.ts
|
|
3
|
+
/** Clear indentation at paragraph start before Backspace joins paragraphs. */
|
|
4
|
+
declare const clearIndentOnBackspace: Command;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { clearIndentOnBackspace };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { expectParagraphAttrs } from "../attrs/index.js";
|
|
2
|
+
import { TextSelection } from "prosemirror-state";
|
|
3
|
+
//#region src/prosemirror/commands/clearParagraphIndent.ts
|
|
4
|
+
/** Clear indentation at paragraph start before Backspace joins paragraphs. */
|
|
5
|
+
const clearIndentOnBackspace = (state, dispatch) => {
|
|
6
|
+
if (!(state.selection instanceof TextSelection)) return false;
|
|
7
|
+
const { $cursor } = state.selection;
|
|
8
|
+
if (!$cursor || $cursor.parentOffset !== 0 || $cursor.parent.type.name !== "paragraph") return false;
|
|
9
|
+
const attrs = expectParagraphAttrs($cursor.parent);
|
|
10
|
+
const hasFirstLine = typeof attrs.indentFirstLine === "number" && attrs.indentFirstLine > 0;
|
|
11
|
+
const hasIndentLeft = typeof attrs.indentLeft === "number" && attrs.indentLeft > 0;
|
|
12
|
+
if (!hasFirstLine && !attrs.hangingIndent && !hasIndentLeft) return false;
|
|
13
|
+
if (dispatch) {
|
|
14
|
+
const position = $cursor.before();
|
|
15
|
+
dispatch(state.tr.setNodeMarkup(position, void 0, {
|
|
16
|
+
...attrs,
|
|
17
|
+
indentFirstLine: null,
|
|
18
|
+
hangingIndent: null,
|
|
19
|
+
indentLeft: null
|
|
20
|
+
}).scrollIntoView());
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { clearIndentOnBackspace };
|
|
@@ -3,30 +3,23 @@ import { EditorView } from "prosemirror-view";
|
|
|
3
3
|
//#region src/prosemirror/commentIdAllocator.d.ts
|
|
4
4
|
/** Sentinel ID for a comment that hasn't been persisted yet (anchored to selection). */
|
|
5
5
|
declare const PENDING_COMMENT_ID = -1;
|
|
6
|
+
declare const CommentIdSpaceExhaustedError_base: import("better-result").TaggedErrorClass<"CommentIdSpaceExhaustedError">;
|
|
7
|
+
declare class CommentIdSpaceExhaustedError extends CommentIdSpaceExhaustedError_base<{
|
|
8
|
+
message: string;
|
|
9
|
+
}> {}
|
|
10
|
+
/** Allocate a valid, unused comment ID in this realm. */
|
|
11
|
+
declare function allocateCommentId(): number;
|
|
12
|
+
/** Reserve a loaded ID and advance past it when the upper bound permits. */
|
|
13
|
+
declare function seedCommentIdAbove(id: number): void;
|
|
6
14
|
type CommentIdAllocator = {
|
|
7
|
-
/** Allocate the next ID
|
|
15
|
+
/** Allocate the next unused ID. */
|
|
8
16
|
next(): number;
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
* loaded comments and tracked-change marks so subsequent allocations don't
|
|
12
|
-
* collide with already-present IDs.
|
|
13
|
-
*/
|
|
14
|
-
seedAbove(maxId: number): void;
|
|
17
|
+
/** Reserve a loaded ID and seed above it when possible. */
|
|
18
|
+
seedAbove(id: number): void;
|
|
15
19
|
};
|
|
16
|
-
/**
|
|
17
|
-
* Create an instance-scoped monotonic comment/revision ID allocator. IDs are
|
|
18
|
-
* never reused (deleting a comment does not free its ID), and the counter is
|
|
19
|
-
* private to this allocator — multiple editors get independent ID spaces.
|
|
20
|
-
*/
|
|
20
|
+
/** Every handle uses the same counter, including separate editor instances. */
|
|
21
21
|
declare function createCommentIdAllocator(): CommentIdAllocator;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
* — comment objects (including replies, which carry no mark) plus
|
|
25
|
-
* tracked-change `revisionId` marks. Because `seedAbove` only ever raises the
|
|
26
|
-
* counter, this is safe to call on load (React) or before each allocation
|
|
27
|
-
* (Vue): new IDs never collide with or reuse an existing one, and the comment
|
|
28
|
-
* and revision ID spaces stay unified.
|
|
29
|
-
*/
|
|
30
|
-
declare function seedCommentAllocator(allocator: CommentIdAllocator, comments: Comment[] | undefined, view: EditorView | null): void;
|
|
22
|
+
/** Seed from comments, replies, and the document's conservative annotation maximum. */
|
|
23
|
+
declare function seedCommentAllocator(allocator: CommentIdAllocator, comments: Comment[] | undefined, view: Pick<EditorView, "state"> | null): void;
|
|
31
24
|
//#endregion
|
|
32
|
-
export { CommentIdAllocator, PENDING_COMMENT_ID, createCommentIdAllocator, seedCommentAllocator };
|
|
25
|
+
export { CommentIdAllocator, CommentIdSpaceExhaustedError, PENDING_COMMENT_ID, allocateCommentId, createCommentIdAllocator, seedCommentAllocator, seedCommentIdAbove };
|
|
@@ -1,40 +1,65 @@
|
|
|
1
1
|
import { maxAnnotationIdInDoc, seedRevisionIdsAbove } from "./plugins/revisionIds.js";
|
|
2
|
+
import { readRangeAnchorAttrs } from "./rangeAnchorAttrs.js";
|
|
3
|
+
import { TaggedError } from "better-result";
|
|
2
4
|
import { MAX_REVISION_ID } from "@stll/docx-core/model";
|
|
3
5
|
//#region src/prosemirror/commentIdAllocator.ts
|
|
4
|
-
/** Sentinel ID for a comment that hasn't been persisted yet (anchored to selection). */
|
|
5
|
-
const PENDING_COMMENT_ID = -1;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* private to this allocator — multiple editors get independent ID spaces.
|
|
7
|
+
* Realm-wide comment ID allocation, shared by live editors and headless reviewers.
|
|
8
|
+
* Loaded and issued IDs stay reserved even when a comment is deleted.
|
|
10
9
|
*/
|
|
10
|
+
/** Sentinel ID for a comment that hasn't been persisted yet (anchored to selection). */
|
|
11
|
+
const PENDING_COMMENT_ID = -1;
|
|
12
|
+
var CommentIdSpaceExhaustedError = class extends TaggedError("CommentIdSpaceExhaustedError") {};
|
|
13
|
+
let nextId = 1;
|
|
14
|
+
const reservedIds = /* @__PURE__ */ new Set();
|
|
15
|
+
/** Allocate a valid, unused comment ID in this realm. */
|
|
16
|
+
function allocateCommentId() {
|
|
17
|
+
if (reservedIds.size >= MAX_REVISION_ID) throw new CommentIdSpaceExhaustedError({ message: "The OOXML comment ID space is exhausted." });
|
|
18
|
+
while (reservedIds.has(nextId)) nextId = nextId === MAX_REVISION_ID ? 1 : nextId + 1;
|
|
19
|
+
const id = nextId;
|
|
20
|
+
reservedIds.add(id);
|
|
21
|
+
nextId = id === MAX_REVISION_ID ? 1 : id + 1;
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
/** Reserve a loaded ID and advance past it when the upper bound permits. */
|
|
25
|
+
function seedCommentIdAbove(id) {
|
|
26
|
+
if (!Number.isInteger(id) || id < 0 || id > MAX_REVISION_ID) return;
|
|
27
|
+
if (id > 0) reservedIds.add(id);
|
|
28
|
+
if (id >= nextId) nextId = id === MAX_REVISION_ID ? 1 : id + 1;
|
|
29
|
+
}
|
|
30
|
+
/** Every handle uses the same counter, including separate editor instances. */
|
|
11
31
|
function createCommentIdAllocator() {
|
|
12
|
-
let nextId = 1;
|
|
13
32
|
return {
|
|
14
|
-
next:
|
|
15
|
-
|
|
16
|
-
return nextId++;
|
|
17
|
-
},
|
|
18
|
-
seedAbove(maxId) {
|
|
19
|
-
if (!Number.isInteger(maxId) || maxId < 0 || maxId >= MAX_REVISION_ID) return;
|
|
20
|
-
if (maxId >= nextId) nextId = maxId + 1;
|
|
21
|
-
}
|
|
33
|
+
next: allocateCommentId,
|
|
34
|
+
seedAbove: seedCommentIdAbove
|
|
22
35
|
};
|
|
23
36
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Seed an allocator above every comment/revision ID currently in the document
|
|
26
|
-
* — comment objects (including replies, which carry no mark) plus
|
|
27
|
-
* tracked-change `revisionId` marks. Because `seedAbove` only ever raises the
|
|
28
|
-
* counter, this is safe to call on load (React) or before each allocation
|
|
29
|
-
* (Vue): new IDs never collide with or reuse an existing one, and the comment
|
|
30
|
-
* and revision ID spaces stay unified.
|
|
31
|
-
*/
|
|
37
|
+
/** Seed from comments, replies, and the document's conservative annotation maximum. */
|
|
32
38
|
function seedCommentAllocator(allocator, comments, view) {
|
|
33
39
|
let max = 0;
|
|
34
|
-
for (const comment of comments ?? [])
|
|
35
|
-
|
|
40
|
+
for (const comment of comments ?? []) {
|
|
41
|
+
allocator.seedAbove(comment.id);
|
|
42
|
+
if (Number.isInteger(comment.id) && comment.id > max && comment.id <= MAX_REVISION_ID) max = comment.id;
|
|
43
|
+
}
|
|
44
|
+
if (view) {
|
|
45
|
+
view.state.doc.descendants((node) => {
|
|
46
|
+
for (const mark of node.marks) {
|
|
47
|
+
const id = mark.attrs["commentId"];
|
|
48
|
+
if (typeof id === "number") allocator.seedAbove(id);
|
|
49
|
+
}
|
|
50
|
+
if (node.type.name === "commentReference") {
|
|
51
|
+
const id = node.attrs["commentId"];
|
|
52
|
+
if (typeof id === "number") allocator.seedAbove(id);
|
|
53
|
+
}
|
|
54
|
+
if (node.type.name === "rangeAnchor") {
|
|
55
|
+
const attrs = readRangeAnchorAttrs(node);
|
|
56
|
+
if (attrs.ok && attrs.value.start.type === "commentRangeStart") allocator.seedAbove(attrs.value.start.id);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
max = Math.max(max, maxAnnotationIdInDoc(view.state.doc));
|
|
60
|
+
}
|
|
36
61
|
allocator.seedAbove(max);
|
|
37
62
|
seedRevisionIdsAbove(max);
|
|
38
63
|
}
|
|
39
64
|
//#endregion
|
|
40
|
-
export { PENDING_COMMENT_ID, createCommentIdAllocator, seedCommentAllocator };
|
|
65
|
+
export { CommentIdSpaceExhaustedError, PENDING_COMMENT_ID, allocateCommentId, createCommentIdAllocator, seedCommentAllocator, seedCommentIdAbove };
|
|
@@ -1,5 +1,97 @@
|
|
|
1
1
|
import { createExtension } from "../create.js";
|
|
2
|
-
import {
|
|
2
|
+
import { Plugin } from "prosemirror-state";
|
|
3
|
+
import { closeHistory, history, isHistoryTransaction, redo, undo } from "prosemirror-history";
|
|
4
|
+
//#region src/prosemirror/extensions/core/HistoryExtension.ts
|
|
5
|
+
/**
|
|
6
|
+
* History Extension — undo/redo via prosemirror-history
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* History adjacency is based on step maps, which differ for physical deletions
|
|
10
|
+
* and suggestion marks. Group continuous typing and same-direction deletions explicitly; each
|
|
11
|
+
* structural edit, paste, drop, or composition starts its own undo event.
|
|
12
|
+
* Capture DOM input before the shared input router or a keymap handles it.
|
|
13
|
+
*/
|
|
14
|
+
const inputBoundaryPlugin = (newGroupDelay) => {
|
|
15
|
+
let previousKind;
|
|
16
|
+
let deletionGroup = 0;
|
|
17
|
+
let previousInputTime = 0;
|
|
18
|
+
let pendingDeletionGroup = null;
|
|
19
|
+
return new Plugin({
|
|
20
|
+
filterTransaction(tr) {
|
|
21
|
+
const pendingGroup = pendingDeletionGroup;
|
|
22
|
+
pendingDeletionGroup = null;
|
|
23
|
+
if (isHistoryTransaction(tr) || !tr.docChanged && tr.selectionSet) {
|
|
24
|
+
previousKind = void 0;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (tr.docChanged && (previousKind === "deleteBackward" || previousKind === "deleteForward" || previousKind === "deletion")) {
|
|
28
|
+
if (pendingGroup !== null) tr.setMeta("composition", pendingGroup);
|
|
29
|
+
else if (!tr.getMeta("appendedTransaction")) previousKind = void 0;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
view(view) {
|
|
34
|
+
const startInput = (kind) => {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
if (kind !== previousKind || kind === "structure" || kind === "paste" || kind === "drop" || now - previousInputTime > newGroupDelay) {
|
|
37
|
+
deletionGroup--;
|
|
38
|
+
view.dispatch(closeHistory(view.state.tr));
|
|
39
|
+
}
|
|
40
|
+
previousInputTime = now;
|
|
41
|
+
previousKind = kind;
|
|
42
|
+
pendingDeletionGroup = kind === "deleteBackward" || kind === "deleteForward" || kind === "deletion" ? deletionGroup : null;
|
|
43
|
+
};
|
|
44
|
+
const keydown = (event) => {
|
|
45
|
+
if (event.isComposing) return;
|
|
46
|
+
if (event.key === "Backspace" || event.key === "Delete") {
|
|
47
|
+
startInput(event.key === "Backspace" ? "deleteBackward" : "deleteForward");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (event.key === "Enter") {
|
|
51
|
+
startInput("structure");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (event.key.length === 1 && !event.metaKey && !event.ctrlKey) startInput("typing");
|
|
55
|
+
};
|
|
56
|
+
const beforeinput = (event) => {
|
|
57
|
+
if (event.inputType === "insertCompositionText" || event.isComposing) {
|
|
58
|
+
startInput("composition");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (event.inputType === "insertText" || event.inputType === "insertReplacementText") {
|
|
62
|
+
startInput("typing");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (event.inputType.startsWith("delete")) if (event.inputType.endsWith("Backward")) startInput("deleteBackward");
|
|
66
|
+
else if (event.inputType.endsWith("Forward")) startInput("deleteForward");
|
|
67
|
+
else startInput("deletion");
|
|
68
|
+
else if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") startInput("structure");
|
|
69
|
+
};
|
|
70
|
+
const paste = () => startInput("paste");
|
|
71
|
+
const drop = () => startInput("drop");
|
|
72
|
+
const compositionstart = () => {
|
|
73
|
+
view.dispatch(closeHistory(view.state.tr));
|
|
74
|
+
previousKind = "composition";
|
|
75
|
+
};
|
|
76
|
+
view.dom.addEventListener("keydown", keydown, true);
|
|
77
|
+
view.dom.addEventListener("beforeinput", beforeinput, true);
|
|
78
|
+
view.dom.addEventListener("paste", paste, true);
|
|
79
|
+
view.dom.addEventListener("drop", drop, true);
|
|
80
|
+
view.dom.addEventListener("compositionstart", compositionstart, true);
|
|
81
|
+
return { destroy() {
|
|
82
|
+
view.dom.removeEventListener("keydown", keydown, true);
|
|
83
|
+
view.dom.removeEventListener("beforeinput", beforeinput, true);
|
|
84
|
+
view.dom.removeEventListener("paste", paste, true);
|
|
85
|
+
view.dom.removeEventListener("drop", drop, true);
|
|
86
|
+
view.dom.removeEventListener("compositionstart", compositionstart, true);
|
|
87
|
+
} };
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const consumeHistoryShortcut = (command) => (state, dispatch, view) => {
|
|
92
|
+
command(state, dispatch, view);
|
|
93
|
+
return true;
|
|
94
|
+
};
|
|
3
95
|
const HistoryExtension = createExtension({
|
|
4
96
|
name: "history",
|
|
5
97
|
defaultOptions: {
|
|
@@ -12,15 +104,15 @@ const HistoryExtension = createExtension({
|
|
|
12
104
|
plugins: [history({
|
|
13
105
|
depth: options.depth,
|
|
14
106
|
newGroupDelay: options.newGroupDelay
|
|
15
|
-
})],
|
|
107
|
+
}), inputBoundaryPlugin(options.newGroupDelay)],
|
|
16
108
|
commands: {
|
|
17
109
|
undo: () => undo,
|
|
18
110
|
redo: () => redo
|
|
19
111
|
},
|
|
20
112
|
...options.shortcuts === "editor" && { keyboardShortcuts: {
|
|
21
|
-
"Mod-z": undo,
|
|
22
|
-
"Mod-y": redo,
|
|
23
|
-
"Mod-Shift-z": redo
|
|
113
|
+
"Mod-z": consumeHistoryShortcut(undo),
|
|
114
|
+
"Mod-y": consumeHistoryShortcut(redo),
|
|
115
|
+
"Mod-Shift-z": consumeHistoryShortcut(redo)
|
|
24
116
|
} }
|
|
25
117
|
};
|
|
26
118
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mintBookmarkId, reserveProseBookmarkIds } from "../../../docx/bookmarkIds.js";
|
|
1
2
|
import { BUILT_IN_STYLE_NAME } from "../../../docx/builtInStyles.js";
|
|
2
3
|
import { PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR } from "../../../docx/paragraphPropertySource.js";
|
|
3
4
|
import { isStyleSourcedParagraphNumbering } from "../../../internal/paragraphFormattingSerialization.js";
|
|
@@ -671,6 +672,7 @@ const ParagraphExtension = createNodeExtension({
|
|
|
671
672
|
if (!dispatch) return true;
|
|
672
673
|
const { schema: s } = state;
|
|
673
674
|
const tr = state.tr;
|
|
675
|
+
reserveProseBookmarkIds(state.doc);
|
|
674
676
|
const bookmarkEntries = [];
|
|
675
677
|
for (const h of headings) {
|
|
676
678
|
const bookmarkName = `_Toc${Math.floor(1e8 + Math.random() * 9e8)}`;
|
|
@@ -683,7 +685,7 @@ const ParagraphExtension = createNodeExtension({
|
|
|
683
685
|
const paragraphNode = tr.doc.resolve(mappedPos).nodeAfter;
|
|
684
686
|
if (paragraphNode && paragraphNode.type.name === "paragraph") {
|
|
685
687
|
const newBookmarks = [...(paragraphNode.attrs["bookmarks"] ?? []).filter((b) => !b.name.startsWith("_Toc")), {
|
|
686
|
-
id:
|
|
688
|
+
id: mintBookmarkId(),
|
|
687
689
|
name: bookmarkName
|
|
688
690
|
}];
|
|
689
691
|
tr.setNodeMarkup(mappedPos, void 0, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mergeTextFormatting } from "../../../utils/textFormattingMerge.js";
|
|
2
2
|
import { expectCharacterStyleMarkAttrs, expectRunFormattingOverrideMarkAttrs } from "../../attrs/index.js";
|
|
3
|
+
import { clearIndentOnBackspace } from "../../commands/clearParagraphIndent.js";
|
|
3
4
|
import { keepSectionBreaksOnSurvivingMarks } from "../../commands/sectionBreak.js";
|
|
4
5
|
import { getDocumentStyleResolver } from "../../plugins/documentStyleState.js";
|
|
5
6
|
import { RUN_FORMATTING_MARK_NAMES } from "../../runFormattingMarkNames.js";
|
|
@@ -8,6 +9,7 @@ import { paragraphAttrsFromResolvedStyle } from "../../styles/resolvedStyleAttrs
|
|
|
8
9
|
import { createExtension } from "../create.js";
|
|
9
10
|
import { marksToTextFormatting, textFormattingToMarks } from "../marks/markUtils.js";
|
|
10
11
|
import { Priority } from "../types.js";
|
|
12
|
+
import { EditorState, TextSelection } from "prosemirror-state";
|
|
11
13
|
import { baseKeymap, deleteSelection, joinBackward, joinForward, selectAll, selectParentNode, splitBlock } from "prosemirror-commands";
|
|
12
14
|
//#region src/prosemirror/extensions/features/BaseKeymapExtension.ts
|
|
13
15
|
/**
|
|
@@ -22,31 +24,6 @@ function chainCommands(...commands) {
|
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
|
-
* Backspace at the start of a paragraph clears first-line indent / hanging indent
|
|
26
|
-
* before joining with the previous paragraph (matches Word behavior).
|
|
27
|
-
*/
|
|
28
|
-
const clearIndentOnBackspace = (state, dispatch) => {
|
|
29
|
-
const { $cursor } = state.selection;
|
|
30
|
-
if (!$cursor) return false;
|
|
31
|
-
if ($cursor.parentOffset !== 0) return false;
|
|
32
|
-
if ($cursor.parent.type.name !== "paragraph") return false;
|
|
33
|
-
const attrs = $cursor.parent.attrs;
|
|
34
|
-
const hasFirstLine = attrs["indentFirstLine"] !== null && attrs["indentFirstLine"] > 0;
|
|
35
|
-
const hasHanging = !!attrs["hangingIndent"];
|
|
36
|
-
const hasIndentLeft = attrs["indentLeft"] !== null && attrs["indentLeft"] > 0;
|
|
37
|
-
if (!hasFirstLine && !hasHanging && !hasIndentLeft) return false;
|
|
38
|
-
if (dispatch) {
|
|
39
|
-
const pos = $cursor.before();
|
|
40
|
-
dispatch(state.tr.setNodeMarkup(pos, void 0, {
|
|
41
|
-
...attrs,
|
|
42
|
-
indentFirstLine: null,
|
|
43
|
-
hangingIndent: null,
|
|
44
|
-
indentLeft: null
|
|
45
|
-
}).scrollIntoView());
|
|
46
|
-
}
|
|
47
|
-
return true;
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
27
|
* Custom Enter handler: splits the block, inherits style-related attrs,
|
|
51
28
|
* clears paragraph borders, and preserves run formatting on the new paragraph.
|
|
52
29
|
*
|
|
@@ -117,16 +94,33 @@ function applyNextParagraphStyle(tr, sourcePara, newPara, resolver) {
|
|
|
117
94
|
tr.setStoredMarks(resolved.runFormatting ? textFormattingToMarks(resolved.runFormatting, tr.doc.type.schema) : []);
|
|
118
95
|
return true;
|
|
119
96
|
}
|
|
97
|
+
const blockBoundaryDeletion = (state) => {
|
|
98
|
+
if (!(state.selection instanceof TextSelection) || state.selection.empty) return null;
|
|
99
|
+
const tr = state.tr.deleteSelection();
|
|
100
|
+
return tr.doc.resolve(tr.mapping.map(state.selection.from)).parent.isTextblock ? null : tr;
|
|
101
|
+
};
|
|
120
102
|
const splitBlockClearBorders = (state, dispatch, view) => {
|
|
121
|
-
const
|
|
103
|
+
const deletion = blockBoundaryDeletion(state);
|
|
104
|
+
const splitState = deletion ? EditorState.create({
|
|
105
|
+
doc: deletion.doc,
|
|
106
|
+
selection: deletion.selection,
|
|
107
|
+
storedMarks: deletion.storedMarks
|
|
108
|
+
}) : state;
|
|
109
|
+
const { $from: preSplitFrom } = splitState.selection;
|
|
122
110
|
const sourcePara = preSplitFrom.parent.type.name === "paragraph" ? preSplitFrom.parent : null;
|
|
123
|
-
const caretFormattingMarks = (
|
|
111
|
+
const caretFormattingMarks = (splitState.storedMarks || preSplitFrom.marks()).filter((mark) => RUN_FORMATTING_MARK_NAMES.has(mark.type.name));
|
|
124
112
|
const resolver = getDocumentStyleResolver(state);
|
|
125
113
|
const splitResult = { tr: null };
|
|
126
|
-
if (!splitBlock(
|
|
114
|
+
if (!splitBlock(splitState, dispatch ? (tr) => {
|
|
127
115
|
splitResult.tr = tr;
|
|
128
116
|
} : void 0, view)) return false;
|
|
129
117
|
if (dispatch && splitResult.tr !== null) {
|
|
118
|
+
if (deletion) {
|
|
119
|
+
for (const step of splitResult.tr.steps) deletion.step(step);
|
|
120
|
+
deletion.setSelection(splitResult.tr.selection.getBookmark().resolve(deletion.doc));
|
|
121
|
+
deletion.setStoredMarks(splitResult.tr.storedMarks);
|
|
122
|
+
splitResult.tr = deletion;
|
|
123
|
+
}
|
|
130
124
|
const tr = splitResult.tr;
|
|
131
125
|
const { $from } = tr.selection;
|
|
132
126
|
const newPara = $from.parent;
|
|
@@ -268,9 +268,9 @@ function createShapeElement(type, w, h, adjustments) {
|
|
|
268
268
|
case "straightConnector1": {
|
|
269
269
|
const el = document.createElementNS(SVG_NS, "line");
|
|
270
270
|
setNum(el, "x1", 0);
|
|
271
|
-
setNum(el, "y1",
|
|
271
|
+
setNum(el, "y1", 0);
|
|
272
272
|
setNum(el, "x2", w);
|
|
273
|
-
setNum(el, "y2", h
|
|
273
|
+
setNum(el, "y2", h);
|
|
274
274
|
return el;
|
|
275
275
|
}
|
|
276
276
|
case "rightBrace": {
|
|
@@ -537,9 +537,11 @@ const ShapeExtension = createNodeExtension({
|
|
|
537
537
|
const strokeWidth = finiteNumber(attrs.outlineWidth) ?? 1;
|
|
538
538
|
const svg = document.createElementNS(SVG_NS, "svg");
|
|
539
539
|
svg.setAttribute("xmlns", SVG_NS);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
svg
|
|
540
|
+
const viewportWidth = w === 0 ? 1 : w;
|
|
541
|
+
const viewportHeight = h === 0 ? 1 : h;
|
|
542
|
+
setNum(svg, "width", viewportWidth);
|
|
543
|
+
setNum(svg, "height", viewportHeight);
|
|
544
|
+
svg.setAttribute("viewBox", `0 0 ${viewportWidth} ${viewportHeight}`);
|
|
543
545
|
let gradient = null;
|
|
544
546
|
let fillAttr;
|
|
545
547
|
if (attrs.fillType === "gradient" && attrs.gradientStops) {
|
|
@@ -548,7 +550,7 @@ const ShapeExtension = createNodeExtension({
|
|
|
548
550
|
fillAttr = gradient ? `url(#${gradId})` : safeFillColor;
|
|
549
551
|
} else fillAttr = attrs.fillType === "none" ? "none" : safeFillColor;
|
|
550
552
|
const strokeAttr = attrs.outlineStyle === "none" ? "none" : safeStrokeColor;
|
|
551
|
-
svg.setAttribute("style", `fill:${fillAttr};stroke:${strokeAttr};stroke-width:${strokeWidth}`);
|
|
553
|
+
svg.setAttribute("style", `fill:${fillAttr};stroke:${strokeAttr};stroke-width:${strokeWidth};overflow:visible`);
|
|
552
554
|
if (gradient) {
|
|
553
555
|
const defs = document.createElementNS(SVG_NS, "defs");
|
|
554
556
|
defs.append(gradient);
|