@stll/folio-core 0.10.0 → 0.11.0
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/controller/collaborationModules.d.ts +7 -0
- package/dist/controller/collaborationModules.js +15 -0
- package/dist/controller/folioEditor.d.ts +1 -1
- package/dist/controller/headerFooterEditorManager.d.ts +41 -0
- package/dist/controller/headerFooterEditorManager.js +182 -0
- package/dist/controller/hiddenEditorManager.d.ts +1 -1
- package/dist/managers/FindReplaceManager.d.ts +8 -0
- package/dist/managers/FindReplaceManager.js +18 -0
- package/dist/managers/TableSelectionManager.d.ts +6 -0
- package/dist/managers/TableSelectionManager.js +14 -0
- package/dist/prosemirror/findReplaceSelection.d.ts +6 -1
- package/dist/prosemirror/findReplaceSelection.js +57 -13
- package/dist/render-dom/BodySelectionOverlay.d.ts +31 -0
- package/dist/render-dom/BodySelectionOverlay.js +76 -0
- package/dist/render-dom/HeaderFooterSelectionOverlay.d.ts +28 -0
- package/dist/render-dom/HeaderFooterSelectionOverlay.js +121 -0
- package/dist/render-dom/RemoteSelectionOverlay.d.ts +30 -0
- package/dist/render-dom/RemoteSelectionOverlay.js +67 -0
- package/dist/render-dom/RenderedDomContext.d.ts +40 -0
- package/dist/render-dom/RenderedDomContext.js +126 -0
- package/dist/render-dom/resolveSidebarItemPositions.d.ts +33 -0
- package/dist/render-dom/resolveSidebarItemPositions.js +45 -0
- package/dist/utils/findReplace.d.ts +32 -0
- package/dist/utils/findReplace.js +118 -0
- package/dist/utils/headerFooter.js +24 -1
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CollaborationModules } from "./hiddenEditorManager.js";
|
|
2
|
+
|
|
3
|
+
//#region src/controller/collaborationModules.d.ts
|
|
4
|
+
/** Lazily load and cache the optional collaboration runtime shared by adapters. */
|
|
5
|
+
declare const loadCollaborationModules: () => Promise<CollaborationModules>;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { loadCollaborationModules };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/controller/collaborationModules.ts
|
|
2
|
+
let collaborationModulesPromise = null;
|
|
3
|
+
/** Lazily load and cache the optional collaboration runtime shared by adapters. */
|
|
4
|
+
const loadCollaborationModules = () => {
|
|
5
|
+
collaborationModulesPromise ??= Promise.all([import("y-prosemirror"), import("yjs")]).then(([yProseMirror, yjs]) => ({
|
|
6
|
+
yProseMirror,
|
|
7
|
+
yjs
|
|
8
|
+
})).catch((error) => {
|
|
9
|
+
collaborationModulesPromise = null;
|
|
10
|
+
throw error;
|
|
11
|
+
});
|
|
12
|
+
return collaborationModulesPromise;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { loadCollaborationModules };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Layout } from "../layout-engine/types.js";
|
|
2
|
-
import { FolioEditorEmitter } from "./folioEditorEvents.js";
|
|
3
2
|
import { HiddenEditorApi } from "./hiddenEditorApi.js";
|
|
3
|
+
import { FolioEditorEmitter } from "./folioEditorEvents.js";
|
|
4
4
|
import { LayoutRunOptions } from "./layoutScheduler.js";
|
|
5
5
|
import { EditorState } from "prosemirror-state";
|
|
6
6
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
import { EditorView } from "prosemirror-view";
|
|
3
|
+
|
|
4
|
+
//#region src/controller/headerFooterEditorManager.d.ts
|
|
5
|
+
type HeaderFooterPartKind = "header" | "footer";
|
|
6
|
+
type HeaderFooterPartKey = {
|
|
7
|
+
kind: HeaderFooterPartKind;
|
|
8
|
+
rId: string;
|
|
9
|
+
};
|
|
10
|
+
type HeaderFooterEditorTransaction = {
|
|
11
|
+
docChanged: boolean;
|
|
12
|
+
kind: HeaderFooterPartKind;
|
|
13
|
+
rId: string;
|
|
14
|
+
selectionChanged: boolean;
|
|
15
|
+
view: EditorView;
|
|
16
|
+
};
|
|
17
|
+
type HeaderFooterEditorManagerDeps = {
|
|
18
|
+
getDocument: () => document_d_exports.Document | null;
|
|
19
|
+
getHost: () => HTMLElement | null;
|
|
20
|
+
getStyles: () => document_d_exports.StyleDefinitions | null | undefined;
|
|
21
|
+
getTheme: () => document_d_exports.Theme | null | undefined;
|
|
22
|
+
onTransaction?: ((transaction: HeaderFooterEditorTransaction) => void) | undefined;
|
|
23
|
+
};
|
|
24
|
+
type HeaderFooterEditorManager = {
|
|
25
|
+
destroy: () => void;
|
|
26
|
+
getView: (rId: string) => EditorView | null;
|
|
27
|
+
listSlots: () => HeaderFooterPartKey[];
|
|
28
|
+
snapshotDocument: (document: document_d_exports.Document) => document_d_exports.Document;
|
|
29
|
+
sync: () => void;
|
|
30
|
+
};
|
|
31
|
+
declare const enumerateHeaderFooterParts: ({
|
|
32
|
+
headers,
|
|
33
|
+
footers
|
|
34
|
+
}: {
|
|
35
|
+
headers: Map<string, document_d_exports.HeaderFooter> | undefined;
|
|
36
|
+
footers: Map<string, document_d_exports.HeaderFooter> | undefined;
|
|
37
|
+
}) => HeaderFooterPartKey[];
|
|
38
|
+
declare const enumerateDocumentHeaderFooterParts: (document: document_d_exports.Document | null) => HeaderFooterPartKey[];
|
|
39
|
+
declare const createHeaderFooterEditorManager: (deps: HeaderFooterEditorManagerDeps) => HeaderFooterEditorManager;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { HeaderFooterEditorManager, HeaderFooterEditorManagerDeps, HeaderFooterEditorTransaction, HeaderFooterPartKey, HeaderFooterPartKind, createHeaderFooterEditorManager, enumerateDocumentHeaderFooterParts, enumerateHeaderFooterParts };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { clearHeaderFooterVerbatimXml } from "../docx/headerFooterVerbatim.js";
|
|
2
|
+
import { proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
|
|
3
|
+
import { ExtensionManager } from "../prosemirror/extensions/ExtensionManager.js";
|
|
4
|
+
import { ensureBaseDirectionInState } from "../prosemirror/extensions/features/AutoBidiDetectionExtension.js";
|
|
5
|
+
import { createDocumentStylesPlugin } from "../prosemirror/plugins/documentStyles.js";
|
|
6
|
+
import { ensureParaIdsInState } from "../prosemirror/extensions/features/ParaIdAllocatorExtension.js";
|
|
7
|
+
import { createStarterKit } from "../prosemirror/extensions/StarterKit.js";
|
|
8
|
+
import { schema } from "../prosemirror/schema/index.js";
|
|
9
|
+
import { headerFooterToProseDoc } from "../prosemirror/conversion/toProseDoc.js";
|
|
10
|
+
import { EditorState } from "prosemirror-state";
|
|
11
|
+
import { EditorView } from "prosemirror-view";
|
|
12
|
+
//#region src/controller/headerFooterEditorManager.ts
|
|
13
|
+
/**
|
|
14
|
+
* Framework-neutral lifecycle owner for persistent header/footer EditorViews.
|
|
15
|
+
*
|
|
16
|
+
* Each distinct relationship id owns one off-screen ProseMirror view. Painted
|
|
17
|
+
* header/footer instances that share that relationship therefore share live
|
|
18
|
+
* editor state, and adapters only decide when to synchronize or destroy the
|
|
19
|
+
* manager.
|
|
20
|
+
*/
|
|
21
|
+
const buildInitialState = (headerFooter, styles, theme, manager) => {
|
|
22
|
+
const proseDocOptions = {};
|
|
23
|
+
if (styles) proseDocOptions.styles = styles;
|
|
24
|
+
if (theme !== void 0) proseDocOptions.theme = theme;
|
|
25
|
+
const document = headerFooterToProseDoc(headerFooter.content, proseDocOptions);
|
|
26
|
+
return ensureBaseDirectionInState(ensureParaIdsInState(EditorState.create({
|
|
27
|
+
doc: document,
|
|
28
|
+
schema,
|
|
29
|
+
plugins: [...manager.getPlugins(), createDocumentStylesPlugin(styles)]
|
|
30
|
+
})));
|
|
31
|
+
};
|
|
32
|
+
const enumerateHeaderFooterParts = ({ headers, footers }) => {
|
|
33
|
+
const parts = [];
|
|
34
|
+
if (headers) for (const rId of headers.keys()) parts.push({
|
|
35
|
+
kind: "header",
|
|
36
|
+
rId
|
|
37
|
+
});
|
|
38
|
+
if (footers) {
|
|
39
|
+
for (const rId of footers.keys()) if (!headers?.has(rId)) parts.push({
|
|
40
|
+
kind: "footer",
|
|
41
|
+
rId
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return parts;
|
|
45
|
+
};
|
|
46
|
+
const enumerateDocumentHeaderFooterParts = (document) => {
|
|
47
|
+
if (!document?.package) return [];
|
|
48
|
+
return enumerateHeaderFooterParts({
|
|
49
|
+
headers: document.package.headers,
|
|
50
|
+
footers: document.package.footers
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const resolveHeaderFooter = (document, { kind, rId }) => {
|
|
54
|
+
return (kind === "header" ? document?.package.headers : document?.package.footers)?.get(rId) ?? null;
|
|
55
|
+
};
|
|
56
|
+
const createHeaderFooterEditorManager = (deps) => {
|
|
57
|
+
const mounted = /* @__PURE__ */ new Map();
|
|
58
|
+
const destroyMounted = (part) => {
|
|
59
|
+
part.view.destroy();
|
|
60
|
+
part.manager.destroy();
|
|
61
|
+
part.mountNode.remove();
|
|
62
|
+
};
|
|
63
|
+
const destroy = () => {
|
|
64
|
+
for (const part of mounted.values()) destroyMounted(part);
|
|
65
|
+
mounted.clear();
|
|
66
|
+
};
|
|
67
|
+
const sync = () => {
|
|
68
|
+
const host = deps.getHost();
|
|
69
|
+
if (!host) return;
|
|
70
|
+
const document = deps.getDocument();
|
|
71
|
+
const styles = deps.getStyles();
|
|
72
|
+
const theme = deps.getTheme();
|
|
73
|
+
const wanted = new Map(enumerateDocumentHeaderFooterParts(document).map((part) => [part.rId, part]));
|
|
74
|
+
for (const [rId, part] of mounted) {
|
|
75
|
+
if (wanted.has(rId)) continue;
|
|
76
|
+
destroyMounted(part);
|
|
77
|
+
mounted.delete(rId);
|
|
78
|
+
}
|
|
79
|
+
for (const part of wanted.values()) {
|
|
80
|
+
const headerFooter = resolveHeaderFooter(document, part);
|
|
81
|
+
if (!headerFooter) continue;
|
|
82
|
+
const existing = mounted.get(part.rId);
|
|
83
|
+
if (existing) {
|
|
84
|
+
if (existing.mountNode.parentElement !== host) host.append(existing.mountNode);
|
|
85
|
+
const contentIsCurrent = existing.appliedHeaderFooter === headerFooter && existing.appliedContent === headerFooter.content;
|
|
86
|
+
const contextIsCurrent = existing.appliedStyles === styles && existing.appliedTheme === theme;
|
|
87
|
+
if (contentIsCurrent && contextIsCurrent) continue;
|
|
88
|
+
existing.view.updateState(buildInitialState(headerFooter, styles, theme, existing.manager));
|
|
89
|
+
existing.appliedHeaderFooter = headerFooter;
|
|
90
|
+
existing.appliedContent = headerFooter.content;
|
|
91
|
+
existing.appliedStyles = styles;
|
|
92
|
+
existing.appliedTheme = theme;
|
|
93
|
+
existing.dirty = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const manager = new ExtensionManager(createStarterKit());
|
|
97
|
+
manager.buildSchema();
|
|
98
|
+
manager.initializeRuntime();
|
|
99
|
+
const mountNode = host.ownerDocument.createElement("div");
|
|
100
|
+
mountNode.dataset["hfRId"] = part.rId;
|
|
101
|
+
mountNode.dataset["hfKind"] = part.kind;
|
|
102
|
+
host.append(mountNode);
|
|
103
|
+
const view = new EditorView(mountNode, {
|
|
104
|
+
state: buildInitialState(headerFooter, styles, theme, manager),
|
|
105
|
+
dispatchTransaction(transaction) {
|
|
106
|
+
const nextState = view.state.apply(transaction);
|
|
107
|
+
view.updateState(nextState);
|
|
108
|
+
const mountedPart = mounted.get(part.rId);
|
|
109
|
+
if (mountedPart && transaction.docChanged) mountedPart.dirty = true;
|
|
110
|
+
deps.onTransaction?.({
|
|
111
|
+
docChanged: transaction.docChanged,
|
|
112
|
+
kind: part.kind,
|
|
113
|
+
rId: part.rId,
|
|
114
|
+
selectionChanged: transaction.selectionSet,
|
|
115
|
+
view
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
mounted.set(part.rId, {
|
|
120
|
+
appliedContent: headerFooter.content,
|
|
121
|
+
appliedHeaderFooter: headerFooter,
|
|
122
|
+
appliedStyles: styles,
|
|
123
|
+
appliedTheme: theme,
|
|
124
|
+
dirty: false,
|
|
125
|
+
kind: part.kind,
|
|
126
|
+
manager,
|
|
127
|
+
mountNode,
|
|
128
|
+
rId: part.rId,
|
|
129
|
+
view
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
destroy,
|
|
135
|
+
getView: (rId) => mounted.get(rId)?.view ?? null,
|
|
136
|
+
listSlots: () => [...mounted.values()].map(({ kind, rId }) => ({
|
|
137
|
+
kind,
|
|
138
|
+
rId
|
|
139
|
+
})),
|
|
140
|
+
snapshotDocument: (document) => {
|
|
141
|
+
let headers = document.package.headers;
|
|
142
|
+
let footers = document.package.footers;
|
|
143
|
+
let headersChanged = false;
|
|
144
|
+
let footersChanged = false;
|
|
145
|
+
for (const { dirty, kind, rId, view } of mounted.values()) {
|
|
146
|
+
if (!dirty) continue;
|
|
147
|
+
const source = kind === "header" ? headers : footers;
|
|
148
|
+
const existing = source?.get(rId);
|
|
149
|
+
if (!source || !existing) continue;
|
|
150
|
+
const updated = {
|
|
151
|
+
...existing,
|
|
152
|
+
content: proseDocToBlocks(view.state.doc)
|
|
153
|
+
};
|
|
154
|
+
clearHeaderFooterVerbatimXml(updated);
|
|
155
|
+
if (kind === "header") {
|
|
156
|
+
if (!headersChanged) {
|
|
157
|
+
headers = new Map(headers);
|
|
158
|
+
headersChanged = true;
|
|
159
|
+
}
|
|
160
|
+
headers?.set(rId, updated);
|
|
161
|
+
} else {
|
|
162
|
+
if (!footersChanged) {
|
|
163
|
+
footers = new Map(footers);
|
|
164
|
+
footersChanged = true;
|
|
165
|
+
}
|
|
166
|
+
footers?.set(rId, updated);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!headersChanged && !footersChanged) return document;
|
|
170
|
+
const packageWithSnapshots = { ...document.package };
|
|
171
|
+
if (headersChanged && headers) packageWithSnapshots.headers = headers;
|
|
172
|
+
if (footersChanged && footers) packageWithSnapshots.footers = footers;
|
|
173
|
+
return {
|
|
174
|
+
...document,
|
|
175
|
+
package: packageWithSnapshots
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
sync
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
//#endregion
|
|
182
|
+
export { createHeaderFooterEditorManager, enumerateDocumentHeaderFooterParts, enumerateHeaderFooterParts };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { document_d_exports } from "../types/document.js";
|
|
2
|
-
import { HiddenEditorApi } from "./hiddenEditorApi.js";
|
|
3
2
|
import { HiddenEditorStateReason } from "../layout-engine/layoutInstrumentation.js";
|
|
4
3
|
import { ExtensionManager } from "../prosemirror/extensions/ExtensionManager.js";
|
|
4
|
+
import { HiddenEditorApi } from "./hiddenEditorApi.js";
|
|
5
5
|
import { EditorState, Plugin, Transaction } from "prosemirror-state";
|
|
6
6
|
import { EditorView } from "prosemirror-view";
|
|
7
7
|
import * as YProseMirror from "y-prosemirror";
|
|
@@ -37,6 +37,14 @@ declare class FindReplaceManager<TMatch extends FindMatchPosition> {
|
|
|
37
37
|
match: TMatch;
|
|
38
38
|
index: number;
|
|
39
39
|
} | null;
|
|
40
|
+
/**
|
|
41
|
+
* Move the cursor to an explicit match index. Returns the selected match, or
|
|
42
|
+
* null when there is no active result or the index is out of bounds.
|
|
43
|
+
*/
|
|
44
|
+
goTo(index: number): {
|
|
45
|
+
match: TMatch;
|
|
46
|
+
index: number;
|
|
47
|
+
} | null;
|
|
40
48
|
/**
|
|
41
49
|
* Replace the current match's text. Returns the new document, or null when
|
|
42
50
|
* there is no current match or the replace fails.
|
|
@@ -61,6 +61,24 @@ var FindReplaceManager = class {
|
|
|
61
61
|
} : null;
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
+
* Move the cursor to an explicit match index. Returns the selected match, or
|
|
65
|
+
* null when there is no active result or the index is out of bounds.
|
|
66
|
+
*/
|
|
67
|
+
goTo(index) {
|
|
68
|
+
const result = this.result;
|
|
69
|
+
if (!result || index < 0 || index >= result.matches.length) return null;
|
|
70
|
+
const match = result.matches.at(index);
|
|
71
|
+
if (!match) return null;
|
|
72
|
+
this.result = {
|
|
73
|
+
...result,
|
|
74
|
+
currentIndex: index
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
match,
|
|
78
|
+
index
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
64
82
|
* Replace the current match's text. Returns the new document, or null when
|
|
65
83
|
* there is no current match or the replace fails.
|
|
66
84
|
*/
|
|
@@ -52,6 +52,12 @@ type TableActionResult = {
|
|
|
52
52
|
};
|
|
53
53
|
declare class TableSelectionManager extends Subscribable<TableSelectionState> {
|
|
54
54
|
constructor();
|
|
55
|
+
/**
|
|
56
|
+
* Track cell coordinates before a document model is available. This supports
|
|
57
|
+
* adapter pointer state without fabricating a table context; callers that
|
|
58
|
+
* need structural operations should use {@link selectCell} with a document.
|
|
59
|
+
*/
|
|
60
|
+
selectCellCoordinates(coords: CellCoordinates): void;
|
|
55
61
|
/**
|
|
56
62
|
* Select a cell within `doc`. Resolves the table at `coords.tableIndex` and
|
|
57
63
|
* derives the full {@link TableContext}. Returns the context, or `null` (and
|
|
@@ -107,6 +107,20 @@ var TableSelectionManager = class extends Subscribable {
|
|
|
107
107
|
super(EMPTY_SELECTION);
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
|
+
* Track cell coordinates before a document model is available. This supports
|
|
111
|
+
* adapter pointer state without fabricating a table context; callers that
|
|
112
|
+
* need structural operations should use {@link selectCell} with a document.
|
|
113
|
+
*/
|
|
114
|
+
selectCellCoordinates(coords) {
|
|
115
|
+
this.setSnapshot({
|
|
116
|
+
context: null,
|
|
117
|
+
table: null,
|
|
118
|
+
tableIndex: coords.tableIndex,
|
|
119
|
+
rowIndex: coords.rowIndex,
|
|
120
|
+
columnIndex: coords.columnIndex
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
110
124
|
* Select a cell within `doc`. Resolves the table at `coords.tableIndex` and
|
|
111
125
|
* derives the full {@link TableContext}. Returns the context, or `null` (and
|
|
112
126
|
* clears the selection) when that table no longer exists.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FindOptions } from "../utils/findReplace.js";
|
|
1
2
|
import { Node } from "prosemirror-model";
|
|
2
3
|
|
|
3
4
|
//#region src/prosemirror/findReplaceSelection.d.ts
|
|
@@ -18,6 +19,10 @@ type FindMatchRange = {
|
|
|
18
19
|
from: number;
|
|
19
20
|
to: number;
|
|
20
21
|
};
|
|
22
|
+
type ProseMirrorFindMatch = FindMatchPosition & FindMatchRange & {
|
|
23
|
+
text: string;
|
|
24
|
+
};
|
|
21
25
|
declare function resolveFindMatchRange(doc: Node, match: FindMatchPosition): FindMatchRange | null;
|
|
26
|
+
declare function findInProseMirrorDocument(doc: Node, searchText: string, options: FindOptions): ProseMirrorFindMatch[];
|
|
22
27
|
//#endregion
|
|
23
|
-
export { FindMatchPosition, FindMatchRange, resolveFindMatchRange };
|
|
28
|
+
export { FindMatchPosition, FindMatchRange, ProseMirrorFindMatch, findInProseMirrorDocument, resolveFindMatchRange };
|
|
@@ -1,29 +1,55 @@
|
|
|
1
|
+
import { findAllMatches } from "../utils/findReplace.js";
|
|
1
2
|
//#region src/prosemirror/findReplaceSelection.ts
|
|
2
3
|
function resolveFindMatchRange(doc, match) {
|
|
3
|
-
let paragraphIndex = 0;
|
|
4
4
|
let resolved = null;
|
|
5
|
-
|
|
6
|
-
if (
|
|
7
|
-
if (paragraphIndex !== match.paragraphIndex) {
|
|
8
|
-
paragraphIndex++;
|
|
9
|
-
return true;
|
|
10
|
-
}
|
|
5
|
+
forEachSearchParagraph(doc, (paragraph, paragraphPos, paragraphIndex) => {
|
|
6
|
+
if (paragraphIndex !== match.paragraphIndex) return true;
|
|
11
7
|
resolved = resolveTextRangeInParagraph({
|
|
12
|
-
paragraph
|
|
13
|
-
paragraphPos
|
|
8
|
+
paragraph,
|
|
9
|
+
paragraphPos,
|
|
14
10
|
startOffset: match.startOffset,
|
|
15
11
|
endOffset: match.endOffset
|
|
16
12
|
});
|
|
17
13
|
return false;
|
|
18
|
-
};
|
|
14
|
+
});
|
|
15
|
+
return resolved;
|
|
16
|
+
}
|
|
17
|
+
function findInProseMirrorDocument(doc, searchText, options) {
|
|
18
|
+
if (!searchText) return [];
|
|
19
|
+
const matches = [];
|
|
20
|
+
forEachSearchParagraph(doc, (paragraph, paragraphPos, paragraphIndex) => {
|
|
21
|
+
const text = getSearchableParagraphText(paragraph);
|
|
22
|
+
for (const { start, end } of findAllMatches(text, searchText, options)) {
|
|
23
|
+
const range = resolveTextRangeInParagraph({
|
|
24
|
+
paragraph,
|
|
25
|
+
paragraphPos,
|
|
26
|
+
startOffset: start,
|
|
27
|
+
endOffset: end
|
|
28
|
+
});
|
|
29
|
+
if (range) matches.push({
|
|
30
|
+
paragraphIndex,
|
|
31
|
+
startOffset: start,
|
|
32
|
+
endOffset: end,
|
|
33
|
+
text: text.slice(start, end),
|
|
34
|
+
...range
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
return matches;
|
|
40
|
+
}
|
|
41
|
+
function forEachSearchParagraph(doc, visit) {
|
|
42
|
+
let paragraphIndex = 0;
|
|
19
43
|
const walkBlocks = (container, contentStart) => {
|
|
20
44
|
let offset = 0;
|
|
21
45
|
for (let childIndex = 0; childIndex < container.childCount; childIndex++) {
|
|
22
46
|
const child = container.child(childIndex);
|
|
23
47
|
const childPos = contentStart + offset;
|
|
24
48
|
if (child.type.name === "paragraph") {
|
|
25
|
-
if (!
|
|
49
|
+
if (!visit(child, childPos, paragraphIndex)) return false;
|
|
50
|
+
paragraphIndex++;
|
|
26
51
|
} else if (child.type.name === "table" && !walkTable(child, childPos)) return false;
|
|
52
|
+
else if (child.type.name === "blockSdt" && !walkBlocks(child, childPos + 1)) return false;
|
|
27
53
|
offset += child.nodeSize;
|
|
28
54
|
}
|
|
29
55
|
return true;
|
|
@@ -53,7 +79,6 @@ function resolveFindMatchRange(doc, match) {
|
|
|
53
79
|
return true;
|
|
54
80
|
};
|
|
55
81
|
walkBlocks(doc, 0);
|
|
56
|
-
return resolved;
|
|
57
82
|
}
|
|
58
83
|
function resolveTextRangeInParagraph({ paragraph, paragraphPos, startOffset, endOffset }) {
|
|
59
84
|
let textOffset = 0;
|
|
@@ -83,5 +108,24 @@ function getSearchTextTokenLength(node) {
|
|
|
83
108
|
if (node.type.name === "tab" || node.type.name === "hardBreak") return 1;
|
|
84
109
|
return 0;
|
|
85
110
|
}
|
|
111
|
+
function getSearchableParagraphText(paragraph) {
|
|
112
|
+
let text = "";
|
|
113
|
+
paragraph.descendants((node) => {
|
|
114
|
+
if (node.isText) {
|
|
115
|
+
text += node.text ?? "";
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (node.type.name === "tab") {
|
|
119
|
+
text += " ";
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (node.type.name === "hardBreak") {
|
|
123
|
+
text += "\n";
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
return text;
|
|
129
|
+
}
|
|
86
130
|
//#endregion
|
|
87
|
-
export { resolveFindMatchRange };
|
|
131
|
+
export { findInProseMirrorDocument, resolveFindMatchRange };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EditorState } from "prosemirror-state";
|
|
2
|
+
|
|
3
|
+
//#region src/render-dom/BodySelectionOverlay.d.ts
|
|
4
|
+
type BodySelectionOverlayResult = {
|
|
5
|
+
type: "text";
|
|
6
|
+
} | {
|
|
7
|
+
type: "image";
|
|
8
|
+
element: HTMLElement;
|
|
9
|
+
pmPos: number;
|
|
10
|
+
};
|
|
11
|
+
type SyncBodySelectionOverlayOptions = {
|
|
12
|
+
pagesContainer: HTMLElement;
|
|
13
|
+
state: EditorState;
|
|
14
|
+
zoom: number;
|
|
15
|
+
zIndex?: number;
|
|
16
|
+
caretColor?: string;
|
|
17
|
+
selectionColor?: string;
|
|
18
|
+
};
|
|
19
|
+
declare class BodySelectionOverlay {
|
|
20
|
+
clear(pagesContainer: HTMLElement): void;
|
|
21
|
+
sync({
|
|
22
|
+
pagesContainer,
|
|
23
|
+
state,
|
|
24
|
+
zoom,
|
|
25
|
+
zIndex,
|
|
26
|
+
caretColor,
|
|
27
|
+
selectionColor
|
|
28
|
+
}: SyncBodySelectionOverlayOptions): BodySelectionOverlayResult;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { BodySelectionOverlay, BodySelectionOverlayResult, SyncBodySelectionOverlayOptions };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { findBodyPmAnchor } from "../layout-bridge/dom/findBodyPmSpans.js";
|
|
2
|
+
import { applyCellSelectionHighlight } from "../layout-bridge/cellSelectionHighlight.js";
|
|
3
|
+
import { findImageElement } from "../layout-painter/imageLayout.js";
|
|
4
|
+
import { getCaretPositionFromDom, getSelectionRectsFromDom } from "../layout-bridge/dom/clickToPositionDom.js";
|
|
5
|
+
import { NodeSelection } from "prosemirror-state";
|
|
6
|
+
//#region src/render-dom/BodySelectionOverlay.ts
|
|
7
|
+
/**
|
|
8
|
+
* Imperative body-selection painter shared by framework adapters. The editable
|
|
9
|
+
* ProseMirror view is hidden, so its caret, range selection, and table-cell
|
|
10
|
+
* selection must be projected onto the paginated DOM.
|
|
11
|
+
*/
|
|
12
|
+
const CARET_CLASS = "folio-body-selection-caret";
|
|
13
|
+
const RANGE_CLASS = "folio-body-selection-rect";
|
|
14
|
+
const OWNED_OVERLAY_SELECTOR = `.${CARET_CLASS}, .${RANGE_CLASS}`;
|
|
15
|
+
const setOverlayBox = (element, rect, pagesContainer, zoom) => {
|
|
16
|
+
const divisor = zoom > 0 ? zoom : 1;
|
|
17
|
+
element.style.left = `${rect.x / divisor + pagesContainer.scrollLeft}px`;
|
|
18
|
+
element.style.top = `${rect.y / divisor + pagesContainer.scrollTop}px`;
|
|
19
|
+
element.style.width = `${rect.width / divisor}px`;
|
|
20
|
+
element.style.height = `${rect.height / divisor}px`;
|
|
21
|
+
};
|
|
22
|
+
var BodySelectionOverlay = class {
|
|
23
|
+
clear(pagesContainer) {
|
|
24
|
+
for (const element of pagesContainer.querySelectorAll(OWNED_OVERLAY_SELECTOR)) element.remove();
|
|
25
|
+
}
|
|
26
|
+
sync({ pagesContainer, state, zoom, zIndex = 10, caretColor = "var(--doc-caret, #000)", selectionColor = "var(--doc-selection, rgba(66, 133, 244, 0.3))" }) {
|
|
27
|
+
this.clear(pagesContainer);
|
|
28
|
+
applyCellSelectionHighlight(pagesContainer, state);
|
|
29
|
+
const { selection } = state;
|
|
30
|
+
if (selection instanceof NodeSelection && selection.node.type.name === "image") {
|
|
31
|
+
const anchor = findBodyPmAnchor(pagesContainer, selection.from);
|
|
32
|
+
const element = anchor ? findImageElement(anchor) : null;
|
|
33
|
+
if (element) return {
|
|
34
|
+
type: "image",
|
|
35
|
+
element,
|
|
36
|
+
pmPos: selection.from
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (selection.empty) {
|
|
40
|
+
const caret = getCaretPositionFromDom(pagesContainer, selection.from, pagesContainer.getBoundingClientRect());
|
|
41
|
+
if (!caret) return { type: "text" };
|
|
42
|
+
const element = pagesContainer.ownerDocument.createElement("div");
|
|
43
|
+
element.className = CARET_CLASS;
|
|
44
|
+
element.dataset["folioCaretRect"] = "";
|
|
45
|
+
element.style.position = "absolute";
|
|
46
|
+
element.style.background = caretColor;
|
|
47
|
+
element.style.pointerEvents = "none";
|
|
48
|
+
element.style.zIndex = String(zIndex);
|
|
49
|
+
element.style.animation = "folio-caret-blink 1060ms steps(1, end) infinite";
|
|
50
|
+
setOverlayBox(element, {
|
|
51
|
+
x: caret.x,
|
|
52
|
+
y: caret.y,
|
|
53
|
+
width: 2 * (zoom > 0 ? zoom : 1),
|
|
54
|
+
height: caret.height
|
|
55
|
+
}, pagesContainer, zoom);
|
|
56
|
+
pagesContainer.appendChild(element);
|
|
57
|
+
return { type: "text" };
|
|
58
|
+
}
|
|
59
|
+
const rects = getSelectionRectsFromDom(pagesContainer, selection.from, selection.to, pagesContainer.getBoundingClientRect());
|
|
60
|
+
for (const rect of rects) {
|
|
61
|
+
const element = pagesContainer.ownerDocument.createElement("div");
|
|
62
|
+
element.className = RANGE_CLASS;
|
|
63
|
+
element.dataset["folioSelectionRect"] = "";
|
|
64
|
+
element.dataset["pageIndex"] = String(rect.pageIndex);
|
|
65
|
+
element.style.position = "absolute";
|
|
66
|
+
element.style.background = selectionColor;
|
|
67
|
+
element.style.pointerEvents = "none";
|
|
68
|
+
element.style.zIndex = String(zIndex);
|
|
69
|
+
setOverlayBox(element, rect, pagesContainer, zoom);
|
|
70
|
+
pagesContainer.appendChild(element);
|
|
71
|
+
}
|
|
72
|
+
return { type: "text" };
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { BodySelectionOverlay };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HfSlotKind } from "../layout-bridge/dom/findHfPmSpans.js";
|
|
2
|
+
|
|
3
|
+
//#region src/render-dom/HeaderFooterSelectionOverlay.d.ts
|
|
4
|
+
type HeaderFooterSelection = {
|
|
5
|
+
from: number;
|
|
6
|
+
kind: HfSlotKind;
|
|
7
|
+
pageNumber?: number;
|
|
8
|
+
rId: string;
|
|
9
|
+
to: number;
|
|
10
|
+
};
|
|
11
|
+
type HeaderFooterSelectionRect = {
|
|
12
|
+
height: number;
|
|
13
|
+
width: number;
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
};
|
|
17
|
+
type HeaderFooterSelectionGeometry = {
|
|
18
|
+
caret: Omit<HeaderFooterSelectionRect, "width"> | null;
|
|
19
|
+
ranges: HeaderFooterSelectionRect[];
|
|
20
|
+
};
|
|
21
|
+
/** Resolve a persistent header/footer PM selection against its painted DOM. */
|
|
22
|
+
declare const resolveHeaderFooterSelectionGeometry: (pagesContainer: HTMLElement, selection: HeaderFooterSelection, zoom: number) => HeaderFooterSelectionGeometry;
|
|
23
|
+
declare class HeaderFooterSelectionOverlay {
|
|
24
|
+
clear(pagesContainer: HTMLElement): void;
|
|
25
|
+
sync(pagesContainer: HTMLElement, selection: HeaderFooterSelection | null, zoom: number): void;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { HeaderFooterSelection, HeaderFooterSelectionGeometry, HeaderFooterSelectionOverlay, HeaderFooterSelectionRect, resolveHeaderFooterSelectionGeometry };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { findHfCaretSpan, findHfPmSpans } from "../layout-bridge/dom/findHfPmSpans.js";
|
|
2
|
+
//#region src/render-dom/HeaderFooterSelectionOverlay.ts
|
|
3
|
+
const CARET_CLASS = "folio-hf-selection-caret";
|
|
4
|
+
const RANGE_CLASS = "folio-hf-selection-rect";
|
|
5
|
+
const OWNED_SELECTOR = `.${CARET_CLASS}, .${RANGE_CLASS}`;
|
|
6
|
+
const TEXT_NODE_TYPE = 3;
|
|
7
|
+
const relativeRect = (rect, containerRect, zoom) => ({
|
|
8
|
+
x: (rect.left - containerRect.left) / zoom,
|
|
9
|
+
y: (rect.top - containerRect.top) / zoom,
|
|
10
|
+
width: rect.width / zoom,
|
|
11
|
+
height: rect.height / zoom
|
|
12
|
+
});
|
|
13
|
+
/** Resolve a persistent header/footer PM selection against its painted DOM. */
|
|
14
|
+
const resolveHeaderFooterSelectionGeometry = (pagesContainer, selection, zoom) => {
|
|
15
|
+
const containerRect = pagesContainer.getBoundingClientRect();
|
|
16
|
+
const zoomDivisor = zoom === 0 ? 1 : zoom;
|
|
17
|
+
const pageScope = selection.pageNumber ? pagesContainer.querySelector(`.layout-page[data-page-number="${selection.pageNumber}"]`) ?? pagesContainer : pagesContainer;
|
|
18
|
+
if (selection.from === selection.to) {
|
|
19
|
+
const hit = findHfCaretSpan(pageScope, selection.kind, selection.rId, selection.from);
|
|
20
|
+
if (!hit) return {
|
|
21
|
+
caret: null,
|
|
22
|
+
ranges: []
|
|
23
|
+
};
|
|
24
|
+
const anchorRect = hit.element.getBoundingClientRect();
|
|
25
|
+
let left = hit.edge === "right" ? anchorRect.right : anchorRect.left;
|
|
26
|
+
let top = anchorRect.top;
|
|
27
|
+
let height = anchorRect.height || 16;
|
|
28
|
+
const pmStart = Number.parseInt(hit.element.dataset["pmStart"] ?? "", 10);
|
|
29
|
+
const pmEnd = Number.parseInt(hit.element.dataset["pmEnd"] ?? "", 10);
|
|
30
|
+
const textNode = hit.element.firstChild;
|
|
31
|
+
if (textNode?.nodeType === TEXT_NODE_TYPE && Number.isFinite(pmStart) && Number.isFinite(pmEnd)) {
|
|
32
|
+
const offset = Math.min(Math.max(0, selection.from - pmStart), textNode.textContent?.length ?? 0);
|
|
33
|
+
const range = hit.element.ownerDocument.createRange();
|
|
34
|
+
range.setStart(textNode, offset);
|
|
35
|
+
range.setEnd(textNode, offset);
|
|
36
|
+
const rangeRect = range.getBoundingClientRect();
|
|
37
|
+
if (rangeRect.height > 0 || rangeRect.width > 0 || rangeRect.left > 0) {
|
|
38
|
+
left = rangeRect.left;
|
|
39
|
+
top = rangeRect.top;
|
|
40
|
+
height = rangeRect.height || height;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
caret: {
|
|
45
|
+
x: (left - containerRect.left) / zoomDivisor,
|
|
46
|
+
y: (top - containerRect.top) / zoomDivisor,
|
|
47
|
+
height: height / zoomDivisor
|
|
48
|
+
},
|
|
49
|
+
ranges: []
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const from = Math.min(selection.from, selection.to);
|
|
53
|
+
const to = Math.max(selection.from, selection.to);
|
|
54
|
+
const ranges = [];
|
|
55
|
+
for (const span of findHfPmSpans(pageScope, selection.kind, selection.rId)) {
|
|
56
|
+
const spanStart = Number.parseInt(span.dataset["pmStart"] ?? "", 10);
|
|
57
|
+
const spanEnd = Number.parseInt(span.dataset["pmEnd"] ?? "", 10);
|
|
58
|
+
if (!Number.isFinite(spanStart) || !Number.isFinite(spanEnd)) continue;
|
|
59
|
+
if (spanEnd <= from || spanStart >= to) continue;
|
|
60
|
+
const textNode = span.firstChild;
|
|
61
|
+
if (textNode?.nodeType === TEXT_NODE_TYPE) {
|
|
62
|
+
const length = textNode.textContent?.length ?? 0;
|
|
63
|
+
const startOffset = Math.max(0, from - spanStart);
|
|
64
|
+
const endOffset = Math.min(length, to - spanStart);
|
|
65
|
+
if (startOffset < endOffset) {
|
|
66
|
+
const range = span.ownerDocument.createRange();
|
|
67
|
+
range.setStart(textNode, startOffset);
|
|
68
|
+
range.setEnd(textNode, endOffset);
|
|
69
|
+
for (const rect of Array.from(range.getClientRects())) ranges.push(relativeRect(rect, containerRect, zoomDivisor));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
ranges.push(relativeRect(span.getBoundingClientRect(), containerRect, zoomDivisor));
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
caret: null,
|
|
77
|
+
ranges
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const createOverlayElement = (pagesContainer, className) => {
|
|
81
|
+
const element = pagesContainer.ownerDocument.createElement("div");
|
|
82
|
+
element.className = className;
|
|
83
|
+
element.style.position = "absolute";
|
|
84
|
+
element.style.pointerEvents = "none";
|
|
85
|
+
return element;
|
|
86
|
+
};
|
|
87
|
+
var HeaderFooterSelectionOverlay = class {
|
|
88
|
+
clear(pagesContainer) {
|
|
89
|
+
for (const element of pagesContainer.querySelectorAll(OWNED_SELECTOR)) element.remove();
|
|
90
|
+
}
|
|
91
|
+
sync(pagesContainer, selection, zoom) {
|
|
92
|
+
this.clear(pagesContainer);
|
|
93
|
+
if (!selection) return;
|
|
94
|
+
const geometry = resolveHeaderFooterSelectionGeometry(pagesContainer, selection, zoom);
|
|
95
|
+
for (const rect of geometry.ranges) {
|
|
96
|
+
const element = createOverlayElement(pagesContainer, RANGE_CLASS);
|
|
97
|
+
element.dataset["testid"] = "hf-selection-rect";
|
|
98
|
+
element.style.left = `${rect.x}px`;
|
|
99
|
+
element.style.top = `${rect.y}px`;
|
|
100
|
+
element.style.width = `${rect.width}px`;
|
|
101
|
+
element.style.height = `${rect.height}px`;
|
|
102
|
+
element.style.background = "var(--doc-selection, rgba(66, 133, 244, 0.3))";
|
|
103
|
+
element.style.opacity = "0.35";
|
|
104
|
+
element.style.zIndex = "10";
|
|
105
|
+
pagesContainer.append(element);
|
|
106
|
+
}
|
|
107
|
+
if (!geometry.caret) return;
|
|
108
|
+
const caret = createOverlayElement(pagesContainer, CARET_CLASS);
|
|
109
|
+
caret.dataset["testid"] = "hf-caret";
|
|
110
|
+
caret.style.left = `${geometry.caret.x}px`;
|
|
111
|
+
caret.style.top = `${geometry.caret.y}px`;
|
|
112
|
+
caret.style.width = "2px";
|
|
113
|
+
caret.style.height = `${geometry.caret.height}px`;
|
|
114
|
+
caret.style.background = "var(--doc-canvas-text, #000)";
|
|
115
|
+
caret.style.zIndex = "11";
|
|
116
|
+
caret.style.animation = "folio-caret-blink 1060ms steps(1, end) infinite";
|
|
117
|
+
pagesContainer.append(caret);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
//#endregion
|
|
121
|
+
export { HeaderFooterSelectionOverlay, resolveHeaderFooterSelectionGeometry };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HiddenProseMirrorRemoteSelection } from "../controller/hiddenEditorManager.js";
|
|
2
|
+
import { RenderedDomContext, RenderedDomPoint, RenderedDomRect } from "./RenderedDomContext.js";
|
|
3
|
+
|
|
4
|
+
//#region src/render-dom/RemoteSelectionOverlay.d.ts
|
|
5
|
+
type SyncRemoteSelectionOverlayOptions = {
|
|
6
|
+
pagesContainer: HTMLElement;
|
|
7
|
+
selections: readonly HiddenProseMirrorRemoteSelection[];
|
|
8
|
+
zoom: number;
|
|
9
|
+
zIndex?: number;
|
|
10
|
+
renderedDomContext?: RenderedDomContext;
|
|
11
|
+
};
|
|
12
|
+
type RemoteSelectionOverlayGeometry = {
|
|
13
|
+
caret: RenderedDomPoint | null;
|
|
14
|
+
rects: RenderedDomRect[];
|
|
15
|
+
selection: HiddenProseMirrorRemoteSelection;
|
|
16
|
+
};
|
|
17
|
+
declare const resolveRemoteSelectionOverlayGeometry: (renderedDomContext: RenderedDomContext, selections: readonly HiddenProseMirrorRemoteSelection[]) => RemoteSelectionOverlayGeometry[];
|
|
18
|
+
/** Paint collaborative cursors and selections over the rendered page DOM. */
|
|
19
|
+
declare class RemoteSelectionOverlay {
|
|
20
|
+
clear(pagesContainer: HTMLElement): void;
|
|
21
|
+
sync({
|
|
22
|
+
pagesContainer,
|
|
23
|
+
selections,
|
|
24
|
+
zoom,
|
|
25
|
+
zIndex,
|
|
26
|
+
renderedDomContext
|
|
27
|
+
}: SyncRemoteSelectionOverlayOptions): void;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { RemoteSelectionOverlay, RemoteSelectionOverlayGeometry, SyncRemoteSelectionOverlayOptions, resolveRemoteSelectionOverlayGeometry };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createRenderedDomContext } from "./RenderedDomContext.js";
|
|
2
|
+
//#region src/render-dom/RemoteSelectionOverlay.ts
|
|
3
|
+
const CARET_CLASS = "folio-remote-selection-caret";
|
|
4
|
+
const LABEL_CLASS = "folio-remote-selection-label";
|
|
5
|
+
const RANGE_CLASS = "folio-remote-selection-rect";
|
|
6
|
+
const OWNED_OVERLAY_SELECTOR = `.${CARET_CLASS}, .${LABEL_CLASS}, .${RANGE_CLASS}`;
|
|
7
|
+
const resolveRemoteSelectionOverlayGeometry = (renderedDomContext, selections) => selections.map((selection) => ({
|
|
8
|
+
caret: renderedDomContext.getCoordinatesForPosition(selection.head),
|
|
9
|
+
rects: renderedDomContext.getRectsForRange(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head)),
|
|
10
|
+
selection
|
|
11
|
+
}));
|
|
12
|
+
const setBox = (element, rect) => {
|
|
13
|
+
element.style.left = `${rect.x}px`;
|
|
14
|
+
element.style.top = `${rect.y}px`;
|
|
15
|
+
element.style.width = `${rect.width}px`;
|
|
16
|
+
element.style.height = `${rect.height}px`;
|
|
17
|
+
};
|
|
18
|
+
const createOverlayElement = (pagesContainer, className, zIndex) => {
|
|
19
|
+
const element = pagesContainer.ownerDocument.createElement("div");
|
|
20
|
+
element.className = className;
|
|
21
|
+
element.style.position = "absolute";
|
|
22
|
+
element.style.pointerEvents = "none";
|
|
23
|
+
element.style.zIndex = String(zIndex);
|
|
24
|
+
return element;
|
|
25
|
+
};
|
|
26
|
+
/** Paint collaborative cursors and selections over the rendered page DOM. */
|
|
27
|
+
var RemoteSelectionOverlay = class {
|
|
28
|
+
clear(pagesContainer) {
|
|
29
|
+
for (const element of pagesContainer.querySelectorAll(OWNED_OVERLAY_SELECTOR)) element.remove();
|
|
30
|
+
}
|
|
31
|
+
sync({ pagesContainer, selections, zoom, zIndex = 20, renderedDomContext = createRenderedDomContext(pagesContainer, zoom) }) {
|
|
32
|
+
this.clear(pagesContainer);
|
|
33
|
+
for (const { caret, rects, selection } of resolveRemoteSelectionOverlayGeometry(renderedDomContext, selections)) {
|
|
34
|
+
for (const rect of rects) {
|
|
35
|
+
const element = createOverlayElement(pagesContainer, RANGE_CLASS, zIndex);
|
|
36
|
+
element.dataset["clientId"] = String(selection.clientId);
|
|
37
|
+
element.style.background = `color-mix(in srgb, ${selection.color} 24%, transparent)`;
|
|
38
|
+
setBox(element, rect);
|
|
39
|
+
pagesContainer.appendChild(element);
|
|
40
|
+
}
|
|
41
|
+
if (!caret) continue;
|
|
42
|
+
const caretElement = createOverlayElement(pagesContainer, CARET_CLASS, zIndex + 1);
|
|
43
|
+
caretElement.dataset["clientId"] = String(selection.clientId);
|
|
44
|
+
caretElement.style.background = selection.color;
|
|
45
|
+
setBox(caretElement, {
|
|
46
|
+
...caret,
|
|
47
|
+
width: 2
|
|
48
|
+
});
|
|
49
|
+
pagesContainer.appendChild(caretElement);
|
|
50
|
+
const label = createOverlayElement(pagesContainer, LABEL_CLASS, zIndex + 2);
|
|
51
|
+
label.dataset["clientId"] = String(selection.clientId);
|
|
52
|
+
label.textContent = selection.name;
|
|
53
|
+
label.style.background = selection.color;
|
|
54
|
+
label.style.color = "var(--background, #fff)";
|
|
55
|
+
label.style.fontSize = "10px";
|
|
56
|
+
label.style.lineHeight = "1";
|
|
57
|
+
label.style.padding = "2px 4px";
|
|
58
|
+
label.style.borderRadius = "2px";
|
|
59
|
+
label.style.left = `${caret.x}px`;
|
|
60
|
+
label.style.top = `${Math.max(0, caret.y - 18)}px`;
|
|
61
|
+
label.style.whiteSpace = "nowrap";
|
|
62
|
+
pagesContainer.appendChild(label);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
export { RemoteSelectionOverlay, resolveRemoteSelectionOverlayGeometry };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/render-dom/RenderedDomContext.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Framework-neutral mapping from ProseMirror positions to the DOM emitted by
|
|
4
|
+
* the layout painter. Framework adapters use this for decorations, anchored
|
|
5
|
+
* chrome, and plugin surfaces without duplicating range geometry.
|
|
6
|
+
*/
|
|
7
|
+
type RenderedDomPoint = {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
type RenderedDomRect = {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
type RenderedDomContext = {
|
|
19
|
+
getCoordinatesForPosition(pmPos: number): RenderedDomPoint | null;
|
|
20
|
+
findElementsForRange(from: number, to: number): Element[];
|
|
21
|
+
getRectsForRange(from: number, to: number): RenderedDomRect[];
|
|
22
|
+
getContainerOffset(): {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
declare class RenderedDomContextImpl implements RenderedDomContext {
|
|
28
|
+
#private;
|
|
29
|
+
constructor(pagesContainer: HTMLElement, zoom?: number);
|
|
30
|
+
getCoordinatesForPosition(pmPos: number): RenderedDomPoint | null;
|
|
31
|
+
findElementsForRange(from: number, to: number): Element[];
|
|
32
|
+
getRectsForRange(from: number, to: number): RenderedDomRect[];
|
|
33
|
+
getContainerOffset(): {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
declare const createRenderedDomContext: (pagesContainer: HTMLElement, zoom?: number) => RenderedDomContext;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { RenderedDomContext, RenderedDomContextImpl, RenderedDomPoint, RenderedDomRect, createRenderedDomContext };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { findBodyEmptyRuns, findBodyPmSpans } from "../layout-bridge/dom/findBodyPmSpans.js";
|
|
2
|
+
import { closestHtmlElement } from "../utils/domGuards.js";
|
|
3
|
+
//#region src/render-dom/RenderedDomContext.ts
|
|
4
|
+
/**
|
|
5
|
+
* Framework-neutral mapping from ProseMirror positions to the DOM emitted by
|
|
6
|
+
* the layout painter. Framework adapters use this for decorations, anchored
|
|
7
|
+
* chrome, and plugin surfaces without duplicating range geometry.
|
|
8
|
+
*/
|
|
9
|
+
const isTextNode = (node) => node?.nodeType === Node.TEXT_NODE;
|
|
10
|
+
const textNodeForSpan = (span) => {
|
|
11
|
+
const firstChild = span.firstChild;
|
|
12
|
+
if (isTextNode(firstChild)) return firstChild;
|
|
13
|
+
if (firstChild instanceof HTMLElement && firstChild.tagName === "A" && isTextNode(firstChild.firstChild)) return firstChild.firstChild;
|
|
14
|
+
return null;
|
|
15
|
+
};
|
|
16
|
+
const lineHeightFor = (element, zoom) => {
|
|
17
|
+
const line = closestHtmlElement(element, ".layout-line");
|
|
18
|
+
return line ? line.getBoundingClientRect().height / zoom : 16;
|
|
19
|
+
};
|
|
20
|
+
var RenderedDomContextImpl = class {
|
|
21
|
+
#pagesContainer;
|
|
22
|
+
#zoom;
|
|
23
|
+
constructor(pagesContainer, zoom = 1) {
|
|
24
|
+
this.#pagesContainer = pagesContainer;
|
|
25
|
+
this.#zoom = zoom > 0 ? zoom : 1;
|
|
26
|
+
}
|
|
27
|
+
getCoordinatesForPosition(pmPos) {
|
|
28
|
+
const containerRect = this.#pagesContainer.getBoundingClientRect();
|
|
29
|
+
for (const span of findBodyPmSpans(this.#pagesContainer)) {
|
|
30
|
+
const pmStart = Number(span.dataset["pmStart"]);
|
|
31
|
+
const pmEnd = Number(span.dataset["pmEnd"]);
|
|
32
|
+
if (!(span.classList.contains("layout-run-tab") ? pmPos >= pmStart && pmPos < pmEnd : pmPos >= pmStart && pmPos <= pmEnd)) continue;
|
|
33
|
+
const spanRect = span.getBoundingClientRect();
|
|
34
|
+
if (span.classList.contains("layout-run-tab")) return {
|
|
35
|
+
x: (spanRect.left - containerRect.left) / this.#zoom,
|
|
36
|
+
y: (spanRect.top - containerRect.top) / this.#zoom,
|
|
37
|
+
height: lineHeightFor(span, this.#zoom)
|
|
38
|
+
};
|
|
39
|
+
const textNode = textNodeForSpan(span);
|
|
40
|
+
if (!textNode) return {
|
|
41
|
+
x: (spanRect.left - containerRect.left) / this.#zoom,
|
|
42
|
+
y: (spanRect.top - containerRect.top) / this.#zoom,
|
|
43
|
+
height: lineHeightFor(span, this.#zoom)
|
|
44
|
+
};
|
|
45
|
+
const charIndex = Math.min(Math.max(0, pmPos - pmStart), textNode.length);
|
|
46
|
+
const range = span.ownerDocument.createRange();
|
|
47
|
+
range.setStart(textNode, charIndex);
|
|
48
|
+
range.setEnd(textNode, charIndex);
|
|
49
|
+
const rangeRect = range.getBoundingClientRect();
|
|
50
|
+
return {
|
|
51
|
+
x: (rangeRect.left - containerRect.left) / this.#zoom,
|
|
52
|
+
y: (rangeRect.top - containerRect.top) / this.#zoom,
|
|
53
|
+
height: lineHeightFor(span, this.#zoom)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
for (const emptyRun of findBodyEmptyRuns(this.#pagesContainer)) {
|
|
57
|
+
const paragraph = closestHtmlElement(emptyRun, ".layout-paragraph");
|
|
58
|
+
if (!paragraph) continue;
|
|
59
|
+
const pmStart = Number(paragraph.dataset["pmStart"]);
|
|
60
|
+
const pmEnd = Number(paragraph.dataset["pmEnd"]);
|
|
61
|
+
if (pmPos < pmStart || pmPos > pmEnd) continue;
|
|
62
|
+
const rect = emptyRun.getBoundingClientRect();
|
|
63
|
+
return {
|
|
64
|
+
x: (rect.left - containerRect.left) / this.#zoom,
|
|
65
|
+
y: (rect.top - containerRect.top) / this.#zoom,
|
|
66
|
+
height: lineHeightFor(emptyRun, this.#zoom)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
findElementsForRange(from, to) {
|
|
72
|
+
return findBodyPmSpans(this.#pagesContainer).filter((span) => {
|
|
73
|
+
const pmStart = Number(span.dataset["pmStart"]);
|
|
74
|
+
return Number(span.dataset["pmEnd"]) > from && pmStart < to;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
getRectsForRange(from, to) {
|
|
78
|
+
const containerRect = this.#pagesContainer.getBoundingClientRect();
|
|
79
|
+
const rects = [];
|
|
80
|
+
for (const element of this.findElementsForRange(from, to)) {
|
|
81
|
+
if (!(element instanceof HTMLElement)) continue;
|
|
82
|
+
const pmStart = Number(element.dataset["pmStart"]);
|
|
83
|
+
if (element.classList.contains("layout-run-tab")) {
|
|
84
|
+
const rect = element.getBoundingClientRect();
|
|
85
|
+
rects.push({
|
|
86
|
+
x: (rect.left - containerRect.left) / this.#zoom,
|
|
87
|
+
y: (rect.top - containerRect.top) / this.#zoom,
|
|
88
|
+
width: rect.width / this.#zoom,
|
|
89
|
+
height: rect.height / this.#zoom
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const textNode = textNodeForSpan(element);
|
|
94
|
+
if (!textNode) continue;
|
|
95
|
+
const startChar = Math.max(0, from - pmStart);
|
|
96
|
+
const endChar = Math.min(textNode.length, to - pmStart);
|
|
97
|
+
if (startChar >= endChar) continue;
|
|
98
|
+
const range = element.ownerDocument.createRange();
|
|
99
|
+
range.setStart(textNode, startChar);
|
|
100
|
+
range.setEnd(textNode, endChar);
|
|
101
|
+
for (const rect of Array.from(range.getClientRects())) rects.push({
|
|
102
|
+
x: (rect.left - containerRect.left) / this.#zoom,
|
|
103
|
+
y: (rect.top - containerRect.top) / this.#zoom,
|
|
104
|
+
width: rect.width / this.#zoom,
|
|
105
|
+
height: rect.height / this.#zoom
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return rects;
|
|
109
|
+
}
|
|
110
|
+
getContainerOffset() {
|
|
111
|
+
const parent = this.#pagesContainer.parentElement;
|
|
112
|
+
if (!parent) return {
|
|
113
|
+
x: 0,
|
|
114
|
+
y: 0
|
|
115
|
+
};
|
|
116
|
+
const containerRect = this.#pagesContainer.getBoundingClientRect();
|
|
117
|
+
const parentRect = parent.getBoundingClientRect();
|
|
118
|
+
return {
|
|
119
|
+
x: (containerRect.left - parentRect.left) / this.#zoom,
|
|
120
|
+
y: (containerRect.top - parentRect.top) / this.#zoom
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const createRenderedDomContext = (pagesContainer, zoom = 1) => new RenderedDomContextImpl(pagesContainer, zoom);
|
|
125
|
+
//#endregion
|
|
126
|
+
export { RenderedDomContextImpl, createRenderedDomContext };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { RenderedDomContext } from "./RenderedDomContext.js";
|
|
2
|
+
|
|
3
|
+
//#region src/render-dom/resolveSidebarItemPositions.d.ts
|
|
4
|
+
type RenderedSidebarItem = {
|
|
5
|
+
id: string;
|
|
6
|
+
anchorPos: number;
|
|
7
|
+
anchorKey?: string;
|
|
8
|
+
priority?: number;
|
|
9
|
+
fixedY?: number;
|
|
10
|
+
estimatedHeight?: number;
|
|
11
|
+
};
|
|
12
|
+
type ResolvedSidebarItemPosition<T extends RenderedSidebarItem = RenderedSidebarItem> = {
|
|
13
|
+
item: T;
|
|
14
|
+
y: number;
|
|
15
|
+
};
|
|
16
|
+
type ResolveSidebarItemPositionsOptions<T extends RenderedSidebarItem> = {
|
|
17
|
+
items: T[];
|
|
18
|
+
anchorPositions: Map<string, number>;
|
|
19
|
+
renderedDomContext: RenderedDomContext | null;
|
|
20
|
+
zoom: number;
|
|
21
|
+
cardHeights: Map<string, number>;
|
|
22
|
+
lastKnown: Map<string, number>;
|
|
23
|
+
};
|
|
24
|
+
declare const resolveSidebarItemPositions: <T extends RenderedSidebarItem>({
|
|
25
|
+
items,
|
|
26
|
+
anchorPositions,
|
|
27
|
+
renderedDomContext,
|
|
28
|
+
zoom,
|
|
29
|
+
cardHeights,
|
|
30
|
+
lastKnown
|
|
31
|
+
}: ResolveSidebarItemPositionsOptions<T>) => ResolvedSidebarItemPosition<T>[];
|
|
32
|
+
//#endregion
|
|
33
|
+
export { RenderedSidebarItem, ResolveSidebarItemPositionsOptions, ResolvedSidebarItemPosition, resolveSidebarItemPositions };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/render-dom/resolveSidebarItemPositions.ts
|
|
2
|
+
const MIN_CARD_GAP = 8;
|
|
3
|
+
const resolveSidebarItemPositions = ({ items, anchorPositions, renderedDomContext, zoom, cardHeights, lastKnown }) => {
|
|
4
|
+
if (items.length === 0) return [];
|
|
5
|
+
const containerOffset = renderedDomContext?.getContainerOffset();
|
|
6
|
+
const positioned = [];
|
|
7
|
+
for (const item of items) {
|
|
8
|
+
let targetY;
|
|
9
|
+
if (item.fixedY !== void 0) targetY = item.fixedY * zoom;
|
|
10
|
+
if (targetY === void 0 && item.anchorKey !== void 0) {
|
|
11
|
+
const anchorY = anchorPositions.get(item.anchorKey);
|
|
12
|
+
if (anchorY !== void 0) targetY = anchorY * zoom;
|
|
13
|
+
}
|
|
14
|
+
if (targetY === void 0 && renderedDomContext && containerOffset) {
|
|
15
|
+
const rect = renderedDomContext.getRectsForRange(item.anchorPos, item.anchorPos + 1).at(0);
|
|
16
|
+
if (rect) targetY = (rect.y + containerOffset.y) * zoom;
|
|
17
|
+
}
|
|
18
|
+
if (targetY === void 0) targetY = lastKnown.get(item.id);
|
|
19
|
+
if (targetY === void 0) continue;
|
|
20
|
+
positioned.push({
|
|
21
|
+
item,
|
|
22
|
+
targetY
|
|
23
|
+
});
|
|
24
|
+
lastKnown.set(item.id, targetY);
|
|
25
|
+
}
|
|
26
|
+
positioned.sort((left, right) => {
|
|
27
|
+
const positionDifference = left.targetY - right.targetY;
|
|
28
|
+
if (positionDifference !== 0) return positionDifference;
|
|
29
|
+
return (left.item.priority ?? 0) - (right.item.priority ?? 0);
|
|
30
|
+
});
|
|
31
|
+
const resolved = [];
|
|
32
|
+
let previousBottom = 0;
|
|
33
|
+
for (const position of positioned) {
|
|
34
|
+
const height = cardHeights.get(position.item.id) ?? position.item.estimatedHeight ?? 80;
|
|
35
|
+
const y = Math.max(position.targetY, previousBottom + MIN_CARD_GAP);
|
|
36
|
+
resolved.push({
|
|
37
|
+
item: position.item,
|
|
38
|
+
y
|
|
39
|
+
});
|
|
40
|
+
previousBottom = y + height;
|
|
41
|
+
}
|
|
42
|
+
return resolved;
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { resolveSidebarItemPositions };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
import { Paragraph } from "../types/content.js";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/findReplace.d.ts
|
|
5
|
+
type FindMatch = {
|
|
6
|
+
paragraphIndex: number;
|
|
7
|
+
contentIndex: number;
|
|
8
|
+
startOffset: number;
|
|
9
|
+
endOffset: number;
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
type FindOptions = {
|
|
13
|
+
matchCase: boolean;
|
|
14
|
+
matchWholeWord: boolean;
|
|
15
|
+
useRegex?: boolean;
|
|
16
|
+
};
|
|
17
|
+
type FindResult = {
|
|
18
|
+
matches: FindMatch[];
|
|
19
|
+
totalCount: number;
|
|
20
|
+
currentIndex: number;
|
|
21
|
+
};
|
|
22
|
+
declare const createDefaultFindOptions: () => FindOptions;
|
|
23
|
+
declare const escapeRegexString: (value: string) => string;
|
|
24
|
+
declare const createSearchPattern: (searchText: string, options: FindOptions) => RegExp | null;
|
|
25
|
+
declare const findAllMatches: (content: string, searchText: string, options: FindOptions) => Array<{
|
|
26
|
+
start: number;
|
|
27
|
+
end: number;
|
|
28
|
+
}>;
|
|
29
|
+
declare const findInDocument: (document: document_d_exports.Document | null | undefined, searchText: string, options: FindOptions) => FindMatch[];
|
|
30
|
+
declare const findInParagraph: (paragraph: Paragraph, searchText: string, options: FindOptions, paragraphIndex: number) => FindMatch[];
|
|
31
|
+
//#endregion
|
|
32
|
+
export { FindMatch, FindOptions, FindResult, createDefaultFindOptions, createSearchPattern, escapeRegexString, findAllMatches, findInDocument, findInParagraph };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
//#region src/utils/findReplace.ts
|
|
2
|
+
const createDefaultFindOptions = () => ({
|
|
3
|
+
matchCase: false,
|
|
4
|
+
matchWholeWord: false,
|
|
5
|
+
useRegex: false
|
|
6
|
+
});
|
|
7
|
+
const escapeRegexString = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
8
|
+
const createSearchPattern = (searchText, options) => {
|
|
9
|
+
if (!searchText) return null;
|
|
10
|
+
try {
|
|
11
|
+
const source = options.useRegex ? searchText : escapeRegexString(searchText);
|
|
12
|
+
const pattern = options.matchWholeWord ? `\\b${source}\\b` : source;
|
|
13
|
+
return new RegExp(pattern, options.matchCase ? "gu" : "giu");
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const findAllMatches = (content, searchText, options) => {
|
|
19
|
+
if (!content || !searchText) return [];
|
|
20
|
+
const searchFor = options.matchCase ? searchText : searchText.toLowerCase();
|
|
21
|
+
const source = escapeRegexString(searchFor);
|
|
22
|
+
const pattern = new RegExp(options.matchWholeWord ? `\\b${source}\\b` : source, options.matchCase ? "gu" : "giu");
|
|
23
|
+
const matches = [];
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
26
|
+
matches.push({
|
|
27
|
+
start: match.index,
|
|
28
|
+
end: match.index + match[0].length
|
|
29
|
+
});
|
|
30
|
+
if (match[0].length === 0) pattern.lastIndex++;
|
|
31
|
+
}
|
|
32
|
+
return matches;
|
|
33
|
+
};
|
|
34
|
+
const findInDocument = (document, searchText, options) => {
|
|
35
|
+
if (!document || !searchText) return [];
|
|
36
|
+
const body = document.package.document;
|
|
37
|
+
if (!isRecord(body) || !Array.isArray(body.content)) return [];
|
|
38
|
+
const matches = [];
|
|
39
|
+
forEachParagraph(body.content, (paragraph, paragraphIndex) => {
|
|
40
|
+
matches.push(...findInParagraph(paragraph, searchText, options, paragraphIndex));
|
|
41
|
+
});
|
|
42
|
+
return matches;
|
|
43
|
+
};
|
|
44
|
+
const findInParagraph = (paragraph, searchText, options, paragraphIndex) => {
|
|
45
|
+
const paragraphText = getParagraphPlainText(paragraph);
|
|
46
|
+
if (!paragraphText) return [];
|
|
47
|
+
return findAllMatches(paragraphText, searchText, options).map(({ start, end }) => ({
|
|
48
|
+
paragraphIndex,
|
|
49
|
+
contentIndex: findContentIndexAtOffset(paragraph, start),
|
|
50
|
+
startOffset: start,
|
|
51
|
+
endOffset: end,
|
|
52
|
+
text: paragraphText.slice(start, end)
|
|
53
|
+
}));
|
|
54
|
+
};
|
|
55
|
+
const forEachParagraph = (blocks, visit) => {
|
|
56
|
+
let paragraphIndex = 0;
|
|
57
|
+
const walkBlocks = (items) => {
|
|
58
|
+
for (const block of items) {
|
|
59
|
+
if (isParagraph(block)) {
|
|
60
|
+
visit(block, paragraphIndex);
|
|
61
|
+
paragraphIndex++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (isTable(block)) {
|
|
65
|
+
walkTable(block);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (isBlockSdt(block)) walkBlocks(block.content);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const walkTable = (table) => {
|
|
72
|
+
for (const row of table.rows) {
|
|
73
|
+
if (!isTableRow(row)) continue;
|
|
74
|
+
for (const cell of row.cells) if (isTableCell(cell)) walkBlocks(cell.content);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walkBlocks(blocks);
|
|
78
|
+
};
|
|
79
|
+
const getRunText = (run) => {
|
|
80
|
+
let text = "";
|
|
81
|
+
for (const item of run.content) if (item.type === "text") text += item.text;
|
|
82
|
+
else if (item.type === "tab") text += " ";
|
|
83
|
+
else if (item.type === "break" && item.breakType === "textWrapping") text += "\n";
|
|
84
|
+
return text;
|
|
85
|
+
};
|
|
86
|
+
const getHyperlinkText = (hyperlink) => {
|
|
87
|
+
let text = "";
|
|
88
|
+
for (const child of hyperlink.children) if (child.type === "run") text += getRunText(child);
|
|
89
|
+
return text;
|
|
90
|
+
};
|
|
91
|
+
const getParagraphContentText = (content) => {
|
|
92
|
+
if (content.type === "run") return getRunText(content);
|
|
93
|
+
if (content.type === "hyperlink") return getHyperlinkText(content);
|
|
94
|
+
if (content.type === "inlineSdt") return content.content.map(getParagraphContentText).join("");
|
|
95
|
+
if (content.type === "simpleField") return content.content.map((child) => child.type === "run" ? getRunText(child) : getHyperlinkText(child)).join("");
|
|
96
|
+
if (content.type === "complexField") return content.fieldResult.map(getRunText).join("");
|
|
97
|
+
return "";
|
|
98
|
+
};
|
|
99
|
+
const getParagraphPlainText = (paragraph) => paragraph.content.map(getParagraphContentText).join("");
|
|
100
|
+
const findContentIndexAtOffset = (paragraph, offset) => {
|
|
101
|
+
let currentOffset = 0;
|
|
102
|
+
for (let contentIndex = 0; contentIndex < paragraph.content.length; contentIndex++) {
|
|
103
|
+
const item = paragraph.content[contentIndex];
|
|
104
|
+
if (!item) continue;
|
|
105
|
+
const itemLength = getParagraphContentText(item).length;
|
|
106
|
+
if (currentOffset + itemLength > offset) return contentIndex;
|
|
107
|
+
currentOffset += itemLength;
|
|
108
|
+
}
|
|
109
|
+
return Math.max(0, paragraph.content.length - 1);
|
|
110
|
+
};
|
|
111
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
112
|
+
const isParagraph = (value) => isRecord(value) && value["type"] === "paragraph" && Array.isArray(value["content"]);
|
|
113
|
+
const isTable = (value) => isRecord(value) && value["type"] === "table" && Array.isArray(value["rows"]);
|
|
114
|
+
const isTableRow = (value) => isRecord(value) && Array.isArray(value["cells"]);
|
|
115
|
+
const isTableCell = (value) => isRecord(value) && Array.isArray(value["content"]);
|
|
116
|
+
const isBlockSdt = (value) => isRecord(value) && value["type"] === "blockSdt" && Array.isArray(value["content"]);
|
|
117
|
+
//#endregion
|
|
118
|
+
export { createDefaultFindOptions, createSearchPattern, escapeRegexString, findAllMatches, findInDocument, findInParagraph };
|
|
@@ -156,7 +156,18 @@ const createEmptyHeaderFooter = (document, position, isFirstPage) => {
|
|
|
156
156
|
const sectionProps = pkg.document.finalSectionProperties;
|
|
157
157
|
if (!sectionProps) return null;
|
|
158
158
|
const hdrFtrType = isFirstPage ? "first" : "default";
|
|
159
|
-
const
|
|
159
|
+
const baseRId = `rId_new_${position}_${hdrFtrType}`;
|
|
160
|
+
const usedRIds = /* @__PURE__ */ new Set([
|
|
161
|
+
...pkg.headers?.keys() ?? [],
|
|
162
|
+
...pkg.footers?.keys() ?? [],
|
|
163
|
+
...pkg.relationships?.keys() ?? []
|
|
164
|
+
]);
|
|
165
|
+
let rId = baseRId;
|
|
166
|
+
let rIdSuffix = 2;
|
|
167
|
+
while (usedRIds.has(rId)) {
|
|
168
|
+
rId = `${baseRId}_${rIdSuffix}`;
|
|
169
|
+
rIdSuffix++;
|
|
170
|
+
}
|
|
160
171
|
const emptyHf = {
|
|
161
172
|
type: position,
|
|
162
173
|
hdrFtrType,
|
|
@@ -174,11 +185,23 @@ const createEmptyHeaderFooter = (document, position, isFirstPage) => {
|
|
|
174
185
|
type: hdrFtrType,
|
|
175
186
|
rId
|
|
176
187
|
};
|
|
188
|
+
const usedTargets = /* @__PURE__ */ new Set();
|
|
189
|
+
for (const relationship of pkg.relationships?.values() ?? []) if (relationship.target) usedTargets.add(relationship.target);
|
|
190
|
+
let targetNumber = 1;
|
|
191
|
+
while (usedTargets.has(`${position}${targetNumber}.xml`)) targetNumber++;
|
|
192
|
+
const relationshipType = position === "header" ? "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" : "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
|
|
193
|
+
const relationships = new Map(pkg.relationships);
|
|
194
|
+
relationships.set(rId, {
|
|
195
|
+
id: rId,
|
|
196
|
+
type: relationshipType,
|
|
197
|
+
target: `${position}${targetNumber}.xml`
|
|
198
|
+
});
|
|
177
199
|
return {
|
|
178
200
|
...document,
|
|
179
201
|
package: {
|
|
180
202
|
...pkg,
|
|
181
203
|
[mapKey]: newMap,
|
|
204
|
+
relationships,
|
|
182
205
|
document: {
|
|
183
206
|
...pkg.document,
|
|
184
207
|
finalSectionProperties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|