@stll/folio-core 0.1.3 → 0.3.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/ai-edits/clean-text.d.ts +21 -1
- package/dist/ai-edits/clean-text.js +69 -1
- package/dist/ai-edits/headless.d.ts +49 -22
- package/dist/ai-edits/headless.js +101 -77
- package/dist/ai-edits/index.d.ts +3 -1
- package/dist/ai-edits/index.js +3 -1
- package/dist/ai-edits/read.d.ts +43 -0
- package/dist/ai-edits/read.js +91 -0
- package/dist/docx/index.d.ts +2 -0
- package/dist/docx/index.js +2 -0
- package/dist/docx/parser.js +1 -1
- package/dist/docx/wrapTypes.d.ts +8 -1
- package/dist/docx/wrapTypes.js +0 -8
- package/dist/i18n/messages/catalogs.gen.d.ts +9899 -0
- package/dist/i18n/messages/catalogs.gen.js +9831 -0
- package/dist/i18n/messages/messages.gen.d.ts +585 -0
- package/dist/i18n/messages/messages.gen.js +0 -0
- package/dist/i18n/messages.d.ts +13 -0
- package/dist/i18n/messages.js +39 -0
- package/dist/layout-bridge/cellSelectionHighlight.d.ts +18 -0
- package/dist/layout-bridge/cellSelectionHighlight.js +36 -0
- package/dist/layout-bridge/headerFooterLayout.d.ts +50 -0
- package/dist/layout-bridge/headerFooterLayout.js +216 -0
- package/dist/layout-bridge/measuring/measureBlocksPipeline.d.ts +33 -0
- package/dist/layout-bridge/measuring/measureBlocksPipeline.js +0 -0
- package/dist/layout-bridge/sectionColumns.d.ts +11 -0
- package/dist/layout-bridge/sectionColumns.js +21 -0
- package/dist/layout-bridge/tableInsertHover.d.ts +42 -0
- package/dist/layout-bridge/tableInsertHover.js +102 -0
- package/dist/layout-engine/index.d.ts +2 -1
- package/dist/layout-engine/index.js +2 -1
- package/dist/layout-engine/measure/measureParagraph.js +40 -5
- package/dist/layout-engine/pmPageIndex.d.ts +24 -0
- package/dist/layout-engine/pmPageIndex.js +37 -0
- package/dist/layout-painter/imageLayout.d.ts +101 -0
- package/dist/layout-painter/imageLayout.js +161 -0
- package/dist/layout-painter/renderPage.d.ts +16 -1
- package/dist/layout-painter/renderPage.js +28 -1
- package/dist/layout-painter/renderParagraph.js +1 -1
- package/dist/layout-painter/sdtBoundary.d.ts +24 -1
- package/dist/layout-painter/sdtBoundary.js +40 -1
- package/dist/managers/AutoSaveManager.d.ts +89 -0
- package/dist/managers/AutoSaveManager.js +279 -0
- package/dist/managers/ClipboardManager.d.ts +31 -0
- package/dist/managers/ClipboardManager.js +147 -0
- package/dist/paged-layout/rangeProjection.d.ts +23 -1
- package/dist/paged-layout/rangeProjection.js +34 -1
- package/dist/prosemirror/cellDragSelection.d.ts +34 -0
- package/dist/prosemirror/cellDragSelection.js +69 -0
- package/dist/prosemirror/commands/sectionBreak.d.ts +15 -0
- package/dist/prosemirror/commands/sectionBreak.js +54 -0
- package/dist/prosemirror/commentIdAllocator.d.ts +32 -0
- package/dist/prosemirror/commentIdAllocator.js +35 -0
- package/dist/prosemirror/commentOps.d.ts +35 -0
- package/dist/prosemirror/commentOps.js +104 -0
- package/dist/prosemirror/conversion/toProseDoc.js +29 -6
- package/dist/prosemirror/extensions/index.d.ts +3 -0
- package/dist/prosemirror/extensions/index.js +3 -0
- package/dist/prosemirror/imageCommit.d.ts +42 -0
- package/dist/prosemirror/imageCommit.js +118 -0
- package/dist/prosemirror/paraText.d.ts +26 -0
- package/dist/prosemirror/paraText.js +73 -0
- package/dist/prosemirror/plugins/templateDirectives.d.ts +21 -1
- package/dist/prosemirror/plugins/templateDirectives.js +40 -1
- package/dist/prosemirror/queries.d.ts +46 -0
- package/dist/prosemirror/queries.js +74 -0
- package/dist/prosemirror/styles/styleResolver.d.ts +43 -1
- package/dist/prosemirror/styles/styleResolver.js +34 -0
- package/dist/prosemirror/tableResize.d.ts +49 -0
- package/dist/prosemirror/tableResize.js +162 -0
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +106 -0
- package/dist/prosemirror/utils/extractTrackedChanges.js +379 -0
- package/dist/prosemirror/utils/visualLineNavigation.d.ts +27 -0
- package/dist/prosemirror/utils/visualLineNavigation.js +217 -0
- package/dist/server.d.ts +2 -1
- package/dist/style-engine/index.d.ts +2 -2
- package/dist/style-engine/styleEngine.d.ts +14 -1
- package/dist/style-engine/styleEngine.js +15 -0
- package/dist/utils/colorResolver.d.ts +8 -1
- package/dist/utils/colorResolver.js +12 -1
- package/dist/utils/findVerticalScrollParent.d.ts +20 -0
- package/dist/utils/findVerticalScrollParent.js +30 -0
- package/dist/utils/fontResolver.d.ts +22 -3
- package/dist/utils/fontResolver.js +216 -31
- package/package.json +10 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Subscribable } from "./Subscribable.js";
|
|
2
|
+
//#region src/managers/AutoSaveManager.ts
|
|
3
|
+
const DEFAULT_STORAGE_KEY = "docx-editor-autosave";
|
|
4
|
+
const DEFAULT_INTERVAL = 3e4;
|
|
5
|
+
const DEFAULT_MAX_AGE = 1440 * 60 * 1e3;
|
|
6
|
+
const DEFAULT_DEBOUNCE_DELAY = 2e3;
|
|
7
|
+
const SAVE_VERSION = 1;
|
|
8
|
+
function isLocalStorageAvailable() {
|
|
9
|
+
try {
|
|
10
|
+
const testKey = "__docx_editor_test__";
|
|
11
|
+
localStorage.setItem(testKey, "test");
|
|
12
|
+
localStorage.removeItem(testKey);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function serializeForStorage(document) {
|
|
19
|
+
return JSON.stringify({
|
|
20
|
+
...document,
|
|
21
|
+
originalBuffer: null
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function isDocumentLike(value) {
|
|
25
|
+
return typeof value === "object" && value !== null && "package" in value;
|
|
26
|
+
}
|
|
27
|
+
function parseSavedData(json) {
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(json);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
35
|
+
if (!("document" in parsed) || !("savedAt" in parsed) || !("version" in parsed)) return null;
|
|
36
|
+
const { document, savedAt, version } = parsed;
|
|
37
|
+
if (typeof savedAt !== "string" || typeof version !== "number") return null;
|
|
38
|
+
if (!isDocumentLike(document)) return null;
|
|
39
|
+
return {
|
|
40
|
+
document,
|
|
41
|
+
savedAt,
|
|
42
|
+
version
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isStale(savedAt, maxAge) {
|
|
46
|
+
const savedTime = new Date(savedAt).getTime();
|
|
47
|
+
return Date.now() - savedTime > maxAge;
|
|
48
|
+
}
|
|
49
|
+
var AutoSaveManager = class extends Subscribable {
|
|
50
|
+
storageKey;
|
|
51
|
+
interval;
|
|
52
|
+
maxAge;
|
|
53
|
+
saveOnChange;
|
|
54
|
+
debounceDelay;
|
|
55
|
+
onSaveCallback;
|
|
56
|
+
onErrorCallback;
|
|
57
|
+
onRecoveryAvailableCallback;
|
|
58
|
+
storageAvailable;
|
|
59
|
+
currentDocument = null;
|
|
60
|
+
lastSavedJson = null;
|
|
61
|
+
intervalTimer = null;
|
|
62
|
+
debounceTimer = null;
|
|
63
|
+
status = "idle";
|
|
64
|
+
lastSaveTime = null;
|
|
65
|
+
hasRecoveryData = false;
|
|
66
|
+
isEnabled;
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
super({
|
|
69
|
+
status: "idle",
|
|
70
|
+
lastSaveTime: null,
|
|
71
|
+
hasRecoveryData: false,
|
|
72
|
+
isEnabled: true
|
|
73
|
+
});
|
|
74
|
+
this.storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
75
|
+
this.interval = options.interval ?? DEFAULT_INTERVAL;
|
|
76
|
+
this.maxAge = options.maxAge ?? DEFAULT_MAX_AGE;
|
|
77
|
+
this.saveOnChange = options.saveOnChange ?? true;
|
|
78
|
+
this.debounceDelay = options.debounceDelay ?? DEFAULT_DEBOUNCE_DELAY;
|
|
79
|
+
this.onSaveCallback = options.onSave;
|
|
80
|
+
this.onErrorCallback = options.onError;
|
|
81
|
+
this.onRecoveryAvailableCallback = options.onRecoveryAvailable;
|
|
82
|
+
this.isEnabled = true;
|
|
83
|
+
this.storageAvailable = isLocalStorageAvailable();
|
|
84
|
+
this.checkRecoveryData();
|
|
85
|
+
}
|
|
86
|
+
/** Update the current document. Triggers a debounced save if enabled. */
|
|
87
|
+
onDocumentChanged(document) {
|
|
88
|
+
this.currentDocument = document;
|
|
89
|
+
if (this.isEnabled && this.saveOnChange && document && this.storageAvailable) this.debounceSave();
|
|
90
|
+
}
|
|
91
|
+
/** Manually trigger a save. */
|
|
92
|
+
save() {
|
|
93
|
+
if (!this.storageAvailable) {
|
|
94
|
+
this.onErrorCallback?.(/* @__PURE__ */ new Error("localStorage is not available"));
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const doc = this.currentDocument;
|
|
98
|
+
if (!doc) return false;
|
|
99
|
+
this.updateStatus("saving");
|
|
100
|
+
try {
|
|
101
|
+
const serialized = serializeForStorage(doc);
|
|
102
|
+
if (serialized === this.lastSavedJson) {
|
|
103
|
+
this.updateStatus("saved");
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
this.persistToStorage(doc);
|
|
107
|
+
this.lastSavedJson = serialized;
|
|
108
|
+
const saveTime = /* @__PURE__ */ new Date();
|
|
109
|
+
this.lastSaveTime = saveTime;
|
|
110
|
+
this.updateStatus("saved");
|
|
111
|
+
this.onSaveCallback?.(saveTime);
|
|
112
|
+
return true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.updateStatus("error");
|
|
115
|
+
this.onErrorCallback?.(error instanceof Error ? error : new Error(String(error)));
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Clear auto-saved data from storage. */
|
|
120
|
+
clear() {
|
|
121
|
+
if (!this.storageAvailable) return;
|
|
122
|
+
try {
|
|
123
|
+
localStorage.removeItem(this.storageKey);
|
|
124
|
+
this.hasRecoveryData = false;
|
|
125
|
+
this.lastSavedJson = null;
|
|
126
|
+
this.emitSnapshot();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
this.onErrorCallback?.(error instanceof Error ? error : new Error(String(error)));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Get recovery data from storage. */
|
|
132
|
+
getRecoveryData() {
|
|
133
|
+
if (!this.storageAvailable) return null;
|
|
134
|
+
const savedJson = localStorage.getItem(this.storageKey);
|
|
135
|
+
if (!savedJson) return null;
|
|
136
|
+
const savedData = parseSavedData(savedJson);
|
|
137
|
+
if (!savedData) return null;
|
|
138
|
+
if (isStale(savedData.savedAt, this.maxAge)) {
|
|
139
|
+
this.clear();
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return savedData;
|
|
143
|
+
}
|
|
144
|
+
/** Accept recovery and return the document. */
|
|
145
|
+
acceptRecovery() {
|
|
146
|
+
const data = this.getRecoveryData();
|
|
147
|
+
if (!data) return null;
|
|
148
|
+
this.hasRecoveryData = false;
|
|
149
|
+
this.emitSnapshot();
|
|
150
|
+
return data.document;
|
|
151
|
+
}
|
|
152
|
+
/** Dismiss recovery and clear saved data. */
|
|
153
|
+
dismissRecovery() {
|
|
154
|
+
this.clear();
|
|
155
|
+
this.hasRecoveryData = false;
|
|
156
|
+
this.emitSnapshot();
|
|
157
|
+
}
|
|
158
|
+
/** Enable auto-save and start the interval timer. */
|
|
159
|
+
enable() {
|
|
160
|
+
this.isEnabled = true;
|
|
161
|
+
this.startInterval();
|
|
162
|
+
this.emitSnapshot();
|
|
163
|
+
}
|
|
164
|
+
/** Disable auto-save and stop all timers. */
|
|
165
|
+
disable() {
|
|
166
|
+
this.isEnabled = false;
|
|
167
|
+
this.stopTimers();
|
|
168
|
+
this.emitSnapshot();
|
|
169
|
+
}
|
|
170
|
+
/** Start the interval timer. Call after enabling or on init. */
|
|
171
|
+
startInterval() {
|
|
172
|
+
this.stopTimers();
|
|
173
|
+
if (!this.isEnabled || !this.storageAvailable) return;
|
|
174
|
+
this.intervalTimer = setInterval(() => {
|
|
175
|
+
this.save();
|
|
176
|
+
}, this.interval);
|
|
177
|
+
}
|
|
178
|
+
/** Save synchronously on destroy (best-effort). */
|
|
179
|
+
destroy() {
|
|
180
|
+
this.stopTimers();
|
|
181
|
+
if (this.isEnabled && this.currentDocument && this.storageAvailable) try {
|
|
182
|
+
this.persistToStorage(this.currentDocument);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
this.onErrorCallback?.(error instanceof Error ? error : new Error(String(error)));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
checkRecoveryData() {
|
|
188
|
+
if (!this.storageAvailable) return;
|
|
189
|
+
const data = this.getRecoveryData();
|
|
190
|
+
if (data) {
|
|
191
|
+
this.hasRecoveryData = true;
|
|
192
|
+
this.emitSnapshot();
|
|
193
|
+
this.onRecoveryAvailableCallback?.(data);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
persistToStorage(document) {
|
|
197
|
+
const dataToSave = {
|
|
198
|
+
document: {
|
|
199
|
+
...document,
|
|
200
|
+
originalBuffer: null
|
|
201
|
+
},
|
|
202
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
203
|
+
version: SAVE_VERSION
|
|
204
|
+
};
|
|
205
|
+
localStorage.setItem(this.storageKey, JSON.stringify(dataToSave));
|
|
206
|
+
}
|
|
207
|
+
debounceSave() {
|
|
208
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
209
|
+
this.debounceTimer = setTimeout(() => {
|
|
210
|
+
this.save();
|
|
211
|
+
}, this.debounceDelay);
|
|
212
|
+
}
|
|
213
|
+
stopTimers() {
|
|
214
|
+
if (this.intervalTimer) {
|
|
215
|
+
clearInterval(this.intervalTimer);
|
|
216
|
+
this.intervalTimer = null;
|
|
217
|
+
}
|
|
218
|
+
if (this.debounceTimer) {
|
|
219
|
+
clearTimeout(this.debounceTimer);
|
|
220
|
+
this.debounceTimer = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
updateStatus(status) {
|
|
224
|
+
this.status = status;
|
|
225
|
+
this.emitSnapshot();
|
|
226
|
+
}
|
|
227
|
+
emitSnapshot() {
|
|
228
|
+
this.setSnapshot({
|
|
229
|
+
status: this.status,
|
|
230
|
+
lastSaveTime: this.lastSaveTime,
|
|
231
|
+
hasRecoveryData: this.hasRecoveryData,
|
|
232
|
+
isEnabled: this.isEnabled
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
/** Format last save time for display. */
|
|
237
|
+
function formatLastSaveTime(date) {
|
|
238
|
+
if (!date) return "Never";
|
|
239
|
+
const diffMs = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
|
|
240
|
+
const diffSec = Math.floor(diffMs / 1e3);
|
|
241
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
242
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
243
|
+
if (diffSec < 10) return "Just now";
|
|
244
|
+
if (diffSec < 60) return `${diffSec} seconds ago`;
|
|
245
|
+
if (diffMin < 60) return `${diffMin} minute${diffMin === 1 ? "" : "s"} ago`;
|
|
246
|
+
if (diffHour < 24) return `${diffHour} hour${diffHour === 1 ? "" : "s"} ago`;
|
|
247
|
+
return date.toLocaleDateString();
|
|
248
|
+
}
|
|
249
|
+
/** Get auto-save status label. */
|
|
250
|
+
function getAutoSaveStatusLabel(status) {
|
|
251
|
+
return {
|
|
252
|
+
idle: "Ready",
|
|
253
|
+
saving: "Saving...",
|
|
254
|
+
saved: "Saved",
|
|
255
|
+
error: "Save failed"
|
|
256
|
+
}[status];
|
|
257
|
+
}
|
|
258
|
+
/** Get storage size used by auto-save (bytes). */
|
|
259
|
+
function getAutoSaveStorageSize(storageKey = DEFAULT_STORAGE_KEY) {
|
|
260
|
+
try {
|
|
261
|
+
const data = localStorage.getItem(storageKey);
|
|
262
|
+
if (!data) return 0;
|
|
263
|
+
return new Blob([data]).size;
|
|
264
|
+
} catch {
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Format storage size for display. */
|
|
269
|
+
function formatStorageSize(bytes) {
|
|
270
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
271
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
272
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
273
|
+
}
|
|
274
|
+
/** Check whether auto-save is supported. */
|
|
275
|
+
function isAutoSaveSupported() {
|
|
276
|
+
return isLocalStorageAvailable();
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
export { AutoSaveManager, formatLastSaveTime, formatStorageSize, getAutoSaveStatusLabel, getAutoSaveStorageSize, isAutoSaveSupported };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
|
|
3
|
+
//#region src/managers/ClipboardManager.d.ts
|
|
4
|
+
/** Selection data for clipboard operations. */
|
|
5
|
+
type ClipboardSelection = {
|
|
6
|
+
text: string;
|
|
7
|
+
runs: document_d_exports.Run[];
|
|
8
|
+
startParagraphIndex: number;
|
|
9
|
+
startRunIndex: number;
|
|
10
|
+
startOffset: number;
|
|
11
|
+
endParagraphIndex: number;
|
|
12
|
+
endRunIndex: number;
|
|
13
|
+
endOffset: number;
|
|
14
|
+
isMultiParagraph: boolean;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Convert a CSS color string (rgb/rgba/hex) to a 6-char uppercase hex string.
|
|
18
|
+
*
|
|
19
|
+
* NOTE: This parses CSS color *strings*, unlike a numeric-component
|
|
20
|
+
* `rgbToHex(r, g, b)` helper.
|
|
21
|
+
*/
|
|
22
|
+
declare function cssColorToHex(color: string): string | null;
|
|
23
|
+
/** Extract formatting from an HTML element's computed styles. */
|
|
24
|
+
declare function extractFormattingFromElement(element: HTMLElement): document_d_exports.TextFormatting | undefined;
|
|
25
|
+
/** Get selected runs from the current DOM selection. */
|
|
26
|
+
declare function getSelectionRuns(): document_d_exports.Run[];
|
|
27
|
+
/** Create a ClipboardSelection from the current DOM selection. */
|
|
28
|
+
declare function createSelectionFromDOM(): ClipboardSelection | null;
|
|
29
|
+
declare const rgbToHex: typeof cssColorToHex;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ClipboardSelection, createSelectionFromDOM, cssColorToHex, extractFormattingFromElement, getSelectionRuns, rgbToHex };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
//#region src/managers/ClipboardManager.ts
|
|
2
|
+
/**
|
|
3
|
+
* Convert a CSS color string (rgb/rgba/hex) to a 6-char uppercase hex string.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: This parses CSS color *strings*, unlike a numeric-component
|
|
6
|
+
* `rgbToHex(r, g, b)` helper.
|
|
7
|
+
*/
|
|
8
|
+
function cssColorToHex(color) {
|
|
9
|
+
if (!color || color === "transparent" || color === "inherit") return null;
|
|
10
|
+
if (color.startsWith("#")) return color.slice(1).toUpperCase();
|
|
11
|
+
const rgbMatch = /rgba?\((?<r>\d+),\s*(?<g>\d+),\s*(?<b>\d+)/u.exec(color);
|
|
12
|
+
if (rgbMatch?.groups) {
|
|
13
|
+
const { r, g, b } = rgbMatch.groups;
|
|
14
|
+
if (r === void 0 || g === void 0 || b === void 0) return null;
|
|
15
|
+
const rHex = Number.parseInt(r, 10).toString(16).padStart(2, "0");
|
|
16
|
+
const gHex = Number.parseInt(g, 10).toString(16).padStart(2, "0");
|
|
17
|
+
const bHex = Number.parseInt(b, 10).toString(16).padStart(2, "0");
|
|
18
|
+
return (rHex + gHex + bHex).toUpperCase();
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/** Extract formatting from an HTML element's computed styles. */
|
|
23
|
+
function extractFormattingFromElement(element) {
|
|
24
|
+
const style = window.getComputedStyle(element);
|
|
25
|
+
const formatting = {};
|
|
26
|
+
if (style.fontWeight === "bold" || Number.parseInt(style.fontWeight, 10) >= 700) formatting.bold = true;
|
|
27
|
+
if (style.fontStyle === "italic") formatting.italic = true;
|
|
28
|
+
const textDecoration = style.textDecoration || style.textDecorationLine || "";
|
|
29
|
+
if (textDecoration.includes("underline")) formatting.underline = { style: "single" };
|
|
30
|
+
if (textDecoration.includes("line-through")) formatting.strike = true;
|
|
31
|
+
const fontSize = Number.parseFloat(style.fontSize);
|
|
32
|
+
if (!Number.isNaN(fontSize) && fontSize > 0) formatting.fontSize = Math.round(fontSize / 1.333 * 2);
|
|
33
|
+
const fontFamily = style.fontFamily.replace(/["']/gu, "").split(",")[0]?.trim();
|
|
34
|
+
if (fontFamily) formatting.fontFamily = { ascii: fontFamily };
|
|
35
|
+
const color = style.color;
|
|
36
|
+
if (color && color !== "rgb(0, 0, 0)") {
|
|
37
|
+
const hex = cssColorToHex(color);
|
|
38
|
+
if (hex) formatting.color = { rgb: hex };
|
|
39
|
+
}
|
|
40
|
+
const bgColor = style.backgroundColor;
|
|
41
|
+
if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
|
|
42
|
+
const hex = cssColorToHex(bgColor);
|
|
43
|
+
if (hex) formatting.shading = { fill: { rgb: hex } };
|
|
44
|
+
}
|
|
45
|
+
return Object.keys(formatting).length > 0 ? formatting : void 0;
|
|
46
|
+
}
|
|
47
|
+
/** Get selected text from a run element, considering partial selection. */
|
|
48
|
+
function getSelectedTextFromRun(runEl, range) {
|
|
49
|
+
const runRange = document.createRange();
|
|
50
|
+
runRange.selectNodeContents(runEl);
|
|
51
|
+
const startInRun = range.compareBoundaryPoints(Range.START_TO_START, runRange) >= 0 && range.compareBoundaryPoints(Range.START_TO_END, runRange) <= 0;
|
|
52
|
+
const endInRun = range.compareBoundaryPoints(Range.END_TO_START, runRange) >= 0 && range.compareBoundaryPoints(Range.END_TO_END, runRange) <= 0;
|
|
53
|
+
if (startInRun && endInRun) return range.toString();
|
|
54
|
+
if (startInRun) {
|
|
55
|
+
const tempRange = document.createRange();
|
|
56
|
+
tempRange.setStart(range.startContainer, range.startOffset);
|
|
57
|
+
tempRange.setEnd(runRange.endContainer, runRange.endOffset);
|
|
58
|
+
return tempRange.toString();
|
|
59
|
+
}
|
|
60
|
+
if (endInRun) {
|
|
61
|
+
const tempRange = document.createRange();
|
|
62
|
+
tempRange.setStart(runRange.startContainer, runRange.startOffset);
|
|
63
|
+
tempRange.setEnd(range.endContainer, range.endOffset);
|
|
64
|
+
return tempRange.toString();
|
|
65
|
+
}
|
|
66
|
+
if (range.intersectsNode(runEl)) return runEl.textContent ?? "";
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
/** Find the paragraph element containing a node. */
|
|
70
|
+
function findParagraphElement(node) {
|
|
71
|
+
let current = node;
|
|
72
|
+
while (current) {
|
|
73
|
+
if (current instanceof HTMLElement && current.hasAttribute("data-paragraph-index")) return current;
|
|
74
|
+
current = current.parentNode;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** Get selected runs from the current DOM selection. */
|
|
79
|
+
function getSelectionRuns() {
|
|
80
|
+
const selection = window.getSelection();
|
|
81
|
+
if (!selection || selection.isCollapsed) return [];
|
|
82
|
+
const runs = [];
|
|
83
|
+
const range = selection.getRangeAt(0);
|
|
84
|
+
const container = range.commonAncestorContainer;
|
|
85
|
+
const containerElement = container instanceof HTMLElement ? container : container.parentElement;
|
|
86
|
+
if (!containerElement) return runs;
|
|
87
|
+
const runElements = containerElement.querySelectorAll(".docx-run");
|
|
88
|
+
for (const runEl of runElements) {
|
|
89
|
+
if (!(runEl instanceof HTMLElement) || !range.intersectsNode(runEl)) continue;
|
|
90
|
+
const text = getSelectedTextFromRun(runEl, range);
|
|
91
|
+
if (!text) continue;
|
|
92
|
+
const formatting = extractFormattingFromElement(runEl);
|
|
93
|
+
const run = formatting ? {
|
|
94
|
+
type: "run",
|
|
95
|
+
formatting,
|
|
96
|
+
content: [{
|
|
97
|
+
type: "text",
|
|
98
|
+
text
|
|
99
|
+
}]
|
|
100
|
+
} : {
|
|
101
|
+
type: "run",
|
|
102
|
+
content: [{
|
|
103
|
+
type: "text",
|
|
104
|
+
text
|
|
105
|
+
}]
|
|
106
|
+
};
|
|
107
|
+
runs.push(run);
|
|
108
|
+
}
|
|
109
|
+
if (runs.length === 0) {
|
|
110
|
+
const selectedText = selection.toString();
|
|
111
|
+
if (selectedText) runs.push({
|
|
112
|
+
type: "run",
|
|
113
|
+
content: [{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: selectedText
|
|
116
|
+
}]
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return runs;
|
|
120
|
+
}
|
|
121
|
+
/** Create a ClipboardSelection from the current DOM selection. */
|
|
122
|
+
function createSelectionFromDOM() {
|
|
123
|
+
const selection = window.getSelection();
|
|
124
|
+
if (!selection || selection.isCollapsed) return null;
|
|
125
|
+
const runs = getSelectionRuns();
|
|
126
|
+
if (runs.length === 0) return null;
|
|
127
|
+
const text = selection.toString();
|
|
128
|
+
const range = selection.getRangeAt(0);
|
|
129
|
+
const startPara = findParagraphElement(range.startContainer);
|
|
130
|
+
const endPara = findParagraphElement(range.endContainer);
|
|
131
|
+
const startParagraphIndex = startPara ? Number.parseInt(startPara.getAttribute("data-paragraph-index") ?? "0", 10) : 0;
|
|
132
|
+
const endParagraphIndex = endPara ? Number.parseInt(endPara.getAttribute("data-paragraph-index") ?? "0", 10) : 0;
|
|
133
|
+
return {
|
|
134
|
+
text,
|
|
135
|
+
runs,
|
|
136
|
+
startParagraphIndex,
|
|
137
|
+
startRunIndex: 0,
|
|
138
|
+
startOffset: range.startOffset,
|
|
139
|
+
endParagraphIndex,
|
|
140
|
+
endRunIndex: 0,
|
|
141
|
+
endOffset: range.endOffset,
|
|
142
|
+
isMultiParagraph: startParagraphIndex !== endParagraphIndex
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const rgbToHex = cssColorToHex;
|
|
146
|
+
//#endregion
|
|
147
|
+
export { createSelectionFromDOM, cssColorToHex, extractFormattingFromElement, getSelectionRuns, rgbToHex };
|
|
@@ -25,6 +25,28 @@ type RangeProjectionContext = {
|
|
|
25
25
|
* left margin is fixed, so this value holds steady as text is edited.
|
|
26
26
|
*/
|
|
27
27
|
declare const measureContentLeft: (pagesContainer: HTMLElement, zoom: number) => number | null;
|
|
28
|
+
/**
|
|
29
|
+
* One page's body-content vertical extent in container space. `.layout-page-content`
|
|
30
|
+
* is inset by the page margins, so its box excludes the header/footer and the
|
|
31
|
+
* inter-page gap — exactly the region a gutter rail is allowed to paint over.
|
|
32
|
+
*/
|
|
33
|
+
type PageContentBand = {
|
|
34
|
+
/** 0-based page index (from `data-page-number` minus one). */pageIndex: number; /** Top of the body content area, container space. */
|
|
35
|
+
top: number; /** Bottom of the body content area, container space. */
|
|
36
|
+
bottom: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Left-margin geometry the block-directive rails need in one measurement pass:
|
|
40
|
+
* the text-column left edge (rail anchor), the usable left-margin width (gutter
|
|
41
|
+
* budget), and each page's body-content band (so a rail can be clipped per page
|
|
42
|
+
* instead of running across page breaks). Measured O(pages), never per-range.
|
|
43
|
+
*/
|
|
44
|
+
type DirectiveGutterGeometry = {
|
|
45
|
+
/** Text-column left edge, container space. Rails anchor here. */contentLeft: number; /** Usable left-margin width: `contentLeft - pageLeftEdge`. Drives the budget. */
|
|
46
|
+
marginWidth: number; /** Per-page body-content vertical bands, in document order. */
|
|
47
|
+
pageBands: PageContentBand[];
|
|
48
|
+
};
|
|
49
|
+
declare const measureDirectiveGutter: (pagesContainer: HTMLElement, zoom: number) => DirectiveGutterGeometry | null;
|
|
28
50
|
/**
|
|
29
51
|
* Computed text style of the painted run at a PM position. Overlays
|
|
30
52
|
* that paint substituted text over the pages (template fill preview,
|
|
@@ -48,4 +70,4 @@ type ProjectedRunFont = {
|
|
|
48
70
|
declare const collectRunFontsAtPmPositions: (pagesContainer: HTMLElement, positions: readonly number[]) => Map<number, ProjectedRunFont>;
|
|
49
71
|
declare const projectRangesToRects: <T extends ProjectableRange>(ranges: readonly T[], ctx: RangeProjectionContext) => Promise<ProjectedRange<T>[]>;
|
|
50
72
|
//#endregion
|
|
51
|
-
export { ProjectableRange, ProjectedRange, ProjectedRunFont, RangeProjectionContext, collectRunFontsAtPmPositions, measureContentLeft, projectRangesToRects };
|
|
73
|
+
export { DirectiveGutterGeometry, PageContentBand, ProjectableRange, ProjectedRange, ProjectedRunFont, RangeProjectionContext, collectRunFontsAtPmPositions, measureContentLeft, measureDirectiveGutter, projectRangesToRects };
|
|
@@ -77,6 +77,39 @@ const measureContentLeft = (pagesContainer, zoom) => {
|
|
|
77
77
|
}
|
|
78
78
|
return Number.isFinite(min) ? (min - overlayLeft) / zoom : null;
|
|
79
79
|
};
|
|
80
|
+
const measureDirectiveGutter = (pagesContainer, zoom) => {
|
|
81
|
+
if (zoom <= 0) return null;
|
|
82
|
+
const overlay = pagesContainer.parentElement?.querySelector("[data-testid=\"selection-overlay\"]");
|
|
83
|
+
const pages = pagesContainer.querySelectorAll(".layout-page");
|
|
84
|
+
if (!overlay || pages.length === 0) return null;
|
|
85
|
+
const overlayRect = overlay.getBoundingClientRect();
|
|
86
|
+
const pageBands = [];
|
|
87
|
+
let contentLeft = null;
|
|
88
|
+
let marginWidth = null;
|
|
89
|
+
for (const [domIndex, page] of pages.entries()) {
|
|
90
|
+
const contentEl = page.querySelector(".layout-page-content");
|
|
91
|
+
if (!contentEl) continue;
|
|
92
|
+
const pageNumberRaw = page.dataset["pageNumber"];
|
|
93
|
+
const pageIndex = pageNumberRaw ? Number(pageNumberRaw) - 1 : domIndex;
|
|
94
|
+
const contentRect = contentEl.getBoundingClientRect();
|
|
95
|
+
pageBands.push({
|
|
96
|
+
pageIndex,
|
|
97
|
+
top: (contentRect.top - overlayRect.top) / zoom,
|
|
98
|
+
bottom: (contentRect.bottom - overlayRect.top) / zoom
|
|
99
|
+
});
|
|
100
|
+
if (contentLeft === null) {
|
|
101
|
+
const pageRect = page.getBoundingClientRect();
|
|
102
|
+
contentLeft = (contentRect.left - overlayRect.left) / zoom;
|
|
103
|
+
marginWidth = Math.max(0, (contentRect.left - pageRect.left) / zoom);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (contentLeft === null || marginWidth === null) return null;
|
|
107
|
+
return {
|
|
108
|
+
contentLeft,
|
|
109
|
+
marginWidth,
|
|
110
|
+
pageBands
|
|
111
|
+
};
|
|
112
|
+
};
|
|
80
113
|
/**
|
|
81
114
|
* Sample the painted-run text style at each position in a single walk
|
|
82
115
|
* over the rendered run spans. Positions outside the rendered buffer
|
|
@@ -150,4 +183,4 @@ const projectRangesToRects = async (ranges, ctx) => {
|
|
|
150
183
|
return result;
|
|
151
184
|
};
|
|
152
185
|
//#endregion
|
|
153
|
-
export { collectRunFontsAtPmPositions, measureContentLeft, projectRangesToRects };
|
|
186
|
+
export { collectRunFontsAtPmPositions, measureContentLeft, measureDirectiveGutter, projectRangesToRects };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EditorView } from "prosemirror-view";
|
|
2
|
+
|
|
3
|
+
//#region src/prosemirror/cellDragSelection.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated No longer used. The tracker ignores pointer overflow so same-cell
|
|
6
|
+
* drags stay text selections. Kept only for backward-compatible imports; will be
|
|
7
|
+
* removed in a future major.
|
|
8
|
+
*/
|
|
9
|
+
declare const CELL_SELECT_OVERFLOW_PX = 5;
|
|
10
|
+
/**
|
|
11
|
+
* Walk up from a PM position to the enclosing `tableCell`/`tableHeader` and
|
|
12
|
+
* return its `before(d)` position (what `CellSelection` resolves against), or
|
|
13
|
+
* null when the position isn't inside a table cell.
|
|
14
|
+
*/
|
|
15
|
+
declare function findCellPosFromPmPos(view: EditorView, pmPos: number): number | null;
|
|
16
|
+
/** Dispatch a `CellSelection` spanning the two cell positions. */
|
|
17
|
+
declare function applyCellSelection(view: EditorView, anchorCellPos: number, headCellPos: number): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Per-gesture state machine for cell-drag selection. Create one per pointer
|
|
20
|
+
* handler; drive it from mousedown/mousemove/mouseup.
|
|
21
|
+
*/
|
|
22
|
+
type CellDragTracker = {
|
|
23
|
+
/** mousedown: record the cell under the press (or null when outside a table). */begin(cellPos: number | null): void;
|
|
24
|
+
/**
|
|
25
|
+
* mousemove: returns true when it applied a `CellSelection` and the caller
|
|
26
|
+
* should NOT also run its text-drag selection for this move.
|
|
27
|
+
*/
|
|
28
|
+
update(view: EditorView, pmPos: number, clientX: number): boolean; /** mouseup / gesture end: clear the transient drag flags. */
|
|
29
|
+
end(): void; /** True while a cell drag is actively producing CellSelections. */
|
|
30
|
+
readonly isCellDragging: boolean;
|
|
31
|
+
};
|
|
32
|
+
declare function createCellDragTracker(): CellDragTracker;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { CELL_SELECT_OVERFLOW_PX, CellDragTracker, applyCellSelection, createCellDragTracker, findCellPosFromPmPos };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CellSelection } from "prosemirror-tables";
|
|
2
|
+
//#region src/prosemirror/cellDragSelection.ts
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated No longer used. The tracker ignores pointer overflow so same-cell
|
|
5
|
+
* drags stay text selections. Kept only for backward-compatible imports; will be
|
|
6
|
+
* removed in a future major.
|
|
7
|
+
*/
|
|
8
|
+
const CELL_SELECT_OVERFLOW_PX = 5;
|
|
9
|
+
/**
|
|
10
|
+
* Walk up from a PM position to the enclosing `tableCell`/`tableHeader` and
|
|
11
|
+
* return its `before(d)` position (what `CellSelection` resolves against), or
|
|
12
|
+
* null when the position isn't inside a table cell.
|
|
13
|
+
*/
|
|
14
|
+
function findCellPosFromPmPos(view, pmPos) {
|
|
15
|
+
try {
|
|
16
|
+
const $pos = view.state.doc.resolve(pmPos);
|
|
17
|
+
for (let d = $pos.depth; d > 0; d--) {
|
|
18
|
+
const node = $pos.node(d);
|
|
19
|
+
if (node.type.name === "tableCell" || node.type.name === "tableHeader") return $pos.before(d);
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** Dispatch a `CellSelection` spanning the two cell positions. */
|
|
25
|
+
function applyCellSelection(view, anchorCellPos, headCellPos) {
|
|
26
|
+
try {
|
|
27
|
+
const $a = view.state.doc.resolve(anchorCellPos);
|
|
28
|
+
const $h = view.state.doc.resolve(headCellPos);
|
|
29
|
+
view.dispatch(view.state.tr.setSelection(new CellSelection($a, $h)));
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function createCellDragTracker() {
|
|
36
|
+
let anchorCellPos = null;
|
|
37
|
+
let cellDragging = false;
|
|
38
|
+
return {
|
|
39
|
+
begin(cellPos) {
|
|
40
|
+
anchorCellPos = cellPos;
|
|
41
|
+
cellDragging = false;
|
|
42
|
+
},
|
|
43
|
+
update(view, pmPos, _clientX) {
|
|
44
|
+
if (anchorCellPos === null) return false;
|
|
45
|
+
if (cellDragging) {
|
|
46
|
+
const cur = findCellPosFromPmPos(view, pmPos);
|
|
47
|
+
if (cur !== null) {
|
|
48
|
+
applyCellSelection(view, anchorCellPos, cur);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const cur = findCellPosFromPmPos(view, pmPos);
|
|
53
|
+
if (cur !== null && cur !== anchorCellPos) {
|
|
54
|
+
cellDragging = true;
|
|
55
|
+
applyCellSelection(view, anchorCellPos, cur);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
},
|
|
60
|
+
end() {
|
|
61
|
+
cellDragging = false;
|
|
62
|
+
},
|
|
63
|
+
get isCellDragging() {
|
|
64
|
+
return cellDragging;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { CELL_SELECT_OVERFLOW_PX, applyCellSelection, createCellDragTracker, findCellPosFromPmPos };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from "prosemirror-state";
|
|
2
|
+
|
|
3
|
+
//#region src/prosemirror/commands/sectionBreak.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Insert a "next page" section break at the cursor — starts a new section on a
|
|
6
|
+
* new page.
|
|
7
|
+
*/
|
|
8
|
+
declare const insertSectionBreakNextPage: Command;
|
|
9
|
+
/**
|
|
10
|
+
* Insert a "continuous" section break at the cursor — starts a new section on
|
|
11
|
+
* the same page.
|
|
12
|
+
*/
|
|
13
|
+
declare const insertSectionBreakContinuous: Command;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { insertSectionBreakContinuous, insertSectionBreakNextPage };
|