@stll/folio-core 0.1.3 → 0.2.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/docx/index.d.ts +2 -0
- package/dist/docx/index.js +2 -0
- package/dist/docx/wrapTypes.d.ts +8 -1
- package/dist/docx/wrapTypes.js +0 -8
- package/dist/i18n/messages/catalogs.gen.d.ts +2776 -0
- package/dist/i18n/messages/catalogs.gen.js +2742 -0
- package/dist/i18n/messages/messages.gen.d.ts +166 -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/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 +9 -1
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { SdtGroup } from "../layout-engine/types.js";
|
|
2
|
+
import { Node } from "prosemirror-model";
|
|
2
3
|
|
|
3
4
|
//#region src/layout-painter/sdtBoundary.d.ts
|
|
4
5
|
declare function applySdtDataAttrs(el: HTMLElement, sdtGroups: readonly SdtGroup[] | undefined): void;
|
|
6
|
+
/**
|
|
7
|
+
* Identities of every block-level content control (`w:sdt`) that encloses the
|
|
8
|
+
* given selection — used to keep a control's boundary visible while the caret
|
|
9
|
+
* is inside it (Word-style focus), independent of mouse hover. Both ends of a
|
|
10
|
+
* range are collected so a selection straddling a control still lights it up.
|
|
11
|
+
*
|
|
12
|
+
* Ported from eigenpal/docx-editor `layout-painter/sdtBoundary.ts`. Upstream
|
|
13
|
+
* keyed the set on `sdt@${pos}` to match per-control boundary boxes; this fork
|
|
14
|
+
* stamps the SDT node's PM position as `data-sdt-pm-pos` on the painted block
|
|
15
|
+
* elements (see `applySdtDataAttrs`), so the identity here is the stringified
|
|
16
|
+
* `$pos.before(d)` — which is exactly that PM position — keeping the returned
|
|
17
|
+
* ids in lockstep with what `applySdtFocus` hit-tests against.
|
|
18
|
+
*/
|
|
19
|
+
declare function enclosingSdtGroupIds(doc: Node, from: number, to: number): Set<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Toggle the `.is-focused` reveal class on every painted block whose enclosing
|
|
22
|
+
* content control encloses the caret. Kept separate from any hover-driven
|
|
23
|
+
* reveal so the two paths never clear each other. Matches the fork's
|
|
24
|
+
* `data-sdt-pm-pos` stamping (upstream matched `.layout-block-sdt-box`'s
|
|
25
|
+
* `data-sdt-group-id`, which this fork does not paint).
|
|
26
|
+
*/
|
|
27
|
+
declare function applySdtFocus(container: HTMLElement, focusedIds: Set<string>): void;
|
|
5
28
|
//#endregion
|
|
6
|
-
export { applySdtDataAttrs };
|
|
29
|
+
export { applySdtDataAttrs, applySdtFocus, enclosingSdtGroupIds };
|
|
@@ -24,5 +24,44 @@ function applySdtDataAttrs(el, sdtGroups) {
|
|
|
24
24
|
alias: g.alias ?? null
|
|
25
25
|
})));
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Identities of every block-level content control (`w:sdt`) that encloses the
|
|
29
|
+
* given selection — used to keep a control's boundary visible while the caret
|
|
30
|
+
* is inside it (Word-style focus), independent of mouse hover. Both ends of a
|
|
31
|
+
* range are collected so a selection straddling a control still lights it up.
|
|
32
|
+
*
|
|
33
|
+
* Ported from eigenpal/docx-editor `layout-painter/sdtBoundary.ts`. Upstream
|
|
34
|
+
* keyed the set on `sdt@${pos}` to match per-control boundary boxes; this fork
|
|
35
|
+
* stamps the SDT node's PM position as `data-sdt-pm-pos` on the painted block
|
|
36
|
+
* elements (see `applySdtDataAttrs`), so the identity here is the stringified
|
|
37
|
+
* `$pos.before(d)` — which is exactly that PM position — keeping the returned
|
|
38
|
+
* ids in lockstep with what `applySdtFocus` hit-tests against.
|
|
39
|
+
*/
|
|
40
|
+
function enclosingSdtGroupIds(doc, from, to) {
|
|
41
|
+
const ids = /* @__PURE__ */ new Set();
|
|
42
|
+
const max = doc.content.size;
|
|
43
|
+
const collect = (pos) => {
|
|
44
|
+
const $pos = doc.resolve(Math.max(0, Math.min(pos, max)));
|
|
45
|
+
for (let d = 1; d <= $pos.depth; d++) if ($pos.node(d).type.name === "blockSdt") ids.add(String($pos.before(d)));
|
|
46
|
+
};
|
|
47
|
+
collect(from);
|
|
48
|
+
if (to !== from) collect(to);
|
|
49
|
+
return ids;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Toggle the `.is-focused` reveal class on every painted block whose enclosing
|
|
53
|
+
* content control encloses the caret. Kept separate from any hover-driven
|
|
54
|
+
* reveal so the two paths never clear each other. Matches the fork's
|
|
55
|
+
* `data-sdt-pm-pos` stamping (upstream matched `.layout-block-sdt-box`'s
|
|
56
|
+
* `data-sdt-group-id`, which this fork does not paint).
|
|
57
|
+
*/
|
|
58
|
+
function applySdtFocus(container, focusedIds) {
|
|
59
|
+
const boxes = container.querySelectorAll("[data-sdt-boundary]");
|
|
60
|
+
for (const box of boxes) {
|
|
61
|
+
const id = box.dataset["sdtPmPos"];
|
|
62
|
+
const on = id != null && focusedIds.has(id);
|
|
63
|
+
if (on !== box.classList.contains("is-focused")) box.classList.toggle("is-focused", on);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
27
66
|
//#endregion
|
|
28
|
-
export { applySdtDataAttrs };
|
|
67
|
+
export { applySdtDataAttrs, applySdtFocus, enclosingSdtGroupIds };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
import { Subscribable } from "./Subscribable.js";
|
|
3
|
+
|
|
4
|
+
//#region src/managers/AutoSaveManager.d.ts
|
|
5
|
+
/** Auto-save status. */
|
|
6
|
+
type AutoSaveStatus = "idle" | "saving" | "saved" | "error";
|
|
7
|
+
/** Configuration for {@link AutoSaveManager}. */
|
|
8
|
+
type AutoSaveManagerOptions = {
|
|
9
|
+
/** Storage key for localStorage (default: 'docx-editor-autosave'). */storageKey?: string; /** Save interval in milliseconds (default: 30000 - 30 seconds). */
|
|
10
|
+
interval?: number; /** Maximum age of auto-save before it is considered stale (default: 24 hours). */
|
|
11
|
+
maxAge?: number; /** Whether to save on document change with debounce (default: true). */
|
|
12
|
+
saveOnChange?: boolean; /** Debounce delay for saveOnChange in milliseconds (default: 2000). */
|
|
13
|
+
debounceDelay?: number; /** Callback when save succeeds. */
|
|
14
|
+
onSave?: (timestamp: Date) => void; /** Callback when save fails. */
|
|
15
|
+
onError?: (error: Error) => void; /** Callback when recovery data is found. */
|
|
16
|
+
onRecoveryAvailable?: (savedDocument: SavedDocumentData) => void;
|
|
17
|
+
};
|
|
18
|
+
/** Saved document data structure. */
|
|
19
|
+
type SavedDocumentData = {
|
|
20
|
+
/** The document. */document: document_d_exports.Document; /** When the document was saved (ISO string). */
|
|
21
|
+
savedAt: string; /** Version for format compatibility. */
|
|
22
|
+
version: number; /** Optional document identifier. */
|
|
23
|
+
documentId?: string;
|
|
24
|
+
};
|
|
25
|
+
/** AutoSaveManager snapshot for UI consumption. */
|
|
26
|
+
type AutoSaveSnapshot = {
|
|
27
|
+
status: AutoSaveStatus;
|
|
28
|
+
lastSaveTime: Date | null;
|
|
29
|
+
hasRecoveryData: boolean;
|
|
30
|
+
isEnabled: boolean;
|
|
31
|
+
};
|
|
32
|
+
declare class AutoSaveManager extends Subscribable<AutoSaveSnapshot> {
|
|
33
|
+
private readonly storageKey;
|
|
34
|
+
private readonly interval;
|
|
35
|
+
private readonly maxAge;
|
|
36
|
+
private readonly saveOnChange;
|
|
37
|
+
private readonly debounceDelay;
|
|
38
|
+
private readonly onSaveCallback;
|
|
39
|
+
private readonly onErrorCallback;
|
|
40
|
+
private readonly onRecoveryAvailableCallback;
|
|
41
|
+
private readonly storageAvailable;
|
|
42
|
+
private currentDocument;
|
|
43
|
+
private lastSavedJson;
|
|
44
|
+
private intervalTimer;
|
|
45
|
+
private debounceTimer;
|
|
46
|
+
private status;
|
|
47
|
+
private lastSaveTime;
|
|
48
|
+
private hasRecoveryData;
|
|
49
|
+
private isEnabled;
|
|
50
|
+
constructor(options?: AutoSaveManagerOptions);
|
|
51
|
+
/** Update the current document. Triggers a debounced save if enabled. */
|
|
52
|
+
onDocumentChanged(document: document_d_exports.Document | null): void;
|
|
53
|
+
/** Manually trigger a save. */
|
|
54
|
+
save(): boolean;
|
|
55
|
+
/** Clear auto-saved data from storage. */
|
|
56
|
+
clear(): void;
|
|
57
|
+
/** Get recovery data from storage. */
|
|
58
|
+
getRecoveryData(): SavedDocumentData | null;
|
|
59
|
+
/** Accept recovery and return the document. */
|
|
60
|
+
acceptRecovery(): document_d_exports.Document | null;
|
|
61
|
+
/** Dismiss recovery and clear saved data. */
|
|
62
|
+
dismissRecovery(): void;
|
|
63
|
+
/** Enable auto-save and start the interval timer. */
|
|
64
|
+
enable(): void;
|
|
65
|
+
/** Disable auto-save and stop all timers. */
|
|
66
|
+
disable(): void;
|
|
67
|
+
/** Start the interval timer. Call after enabling or on init. */
|
|
68
|
+
startInterval(): void;
|
|
69
|
+
/** Save synchronously on destroy (best-effort). */
|
|
70
|
+
destroy(): void;
|
|
71
|
+
private checkRecoveryData;
|
|
72
|
+
private persistToStorage;
|
|
73
|
+
private debounceSave;
|
|
74
|
+
private stopTimers;
|
|
75
|
+
private updateStatus;
|
|
76
|
+
private emitSnapshot;
|
|
77
|
+
}
|
|
78
|
+
/** Format last save time for display. */
|
|
79
|
+
declare function formatLastSaveTime(date: Date | null): string;
|
|
80
|
+
/** Get auto-save status label. */
|
|
81
|
+
declare function getAutoSaveStatusLabel(status: AutoSaveStatus): string;
|
|
82
|
+
/** Get storage size used by auto-save (bytes). */
|
|
83
|
+
declare function getAutoSaveStorageSize(storageKey?: string): number;
|
|
84
|
+
/** Format storage size for display. */
|
|
85
|
+
declare function formatStorageSize(bytes: number): string;
|
|
86
|
+
/** Check whether auto-save is supported. */
|
|
87
|
+
declare function isAutoSaveSupported(): boolean;
|
|
88
|
+
//#endregion
|
|
89
|
+
export { AutoSaveManager, AutoSaveManagerOptions, AutoSaveSnapshot, AutoSaveStatus, SavedDocumentData, formatLastSaveTime, formatStorageSize, getAutoSaveStatusLabel, getAutoSaveStorageSize, isAutoSaveSupported };
|
|
@@ -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 };
|