@yurikilian/lex4 0.1.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/LICENSE +21 -0
- package/dist/__tests__/setup.d.ts +1 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/components/DocumentView.d.ts +10 -0
- package/dist/components/DocumentView.d.ts.map +1 -0
- package/dist/components/EditorSidebar.d.ts +12 -0
- package/dist/components/EditorSidebar.d.ts.map +1 -0
- package/dist/components/HeaderFooterActions.d.ts +16 -0
- package/dist/components/HeaderFooterActions.d.ts.map +1 -0
- package/dist/components/HeaderFooterToggle.d.ts +8 -0
- package/dist/components/HeaderFooterToggle.d.ts.map +1 -0
- package/dist/components/HistorySidebar.d.ts +3 -0
- package/dist/components/HistorySidebar.d.ts.map +1 -0
- package/dist/components/Lex4Editor.d.ts +10 -0
- package/dist/components/Lex4Editor.d.ts.map +1 -0
- package/dist/components/PageBody.d.ts +26 -0
- package/dist/components/PageBody.d.ts.map +1 -0
- package/dist/components/PageFooter.d.ts +18 -0
- package/dist/components/PageFooter.d.ts.map +1 -0
- package/dist/components/PageHeader.d.ts +18 -0
- package/dist/components/PageHeader.d.ts.map +1 -0
- package/dist/components/PageView.d.ts +21 -0
- package/dist/components/PageView.d.ts.map +1 -0
- package/dist/components/Toolbar.d.ts +3 -0
- package/dist/components/Toolbar.d.ts.map +1 -0
- package/dist/components/index.d.ts +12 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/constants/dimensions.d.ts +25 -0
- package/dist/constants/dimensions.d.ts.map +1 -0
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/page-layout.d.ts +11 -0
- package/dist/constants/page-layout.d.ts.map +1 -0
- package/dist/context/document-context.d.ts +108 -0
- package/dist/context/document-context.d.ts.map +1 -0
- package/dist/context/document-provider.d.ts +10 -0
- package/dist/context/document-provider.d.ts.map +1 -0
- package/dist/context/document-reducer.d.ts +8 -0
- package/dist/context/document-reducer.d.ts.map +1 -0
- package/dist/context/index.d.ts +5 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/measure.d.ts +15 -0
- package/dist/engine/measure.d.ts.map +1 -0
- package/dist/engine/overflow.d.ts +28 -0
- package/dist/engine/overflow.d.ts.map +1 -0
- package/dist/engine/paginate.d.ts +12 -0
- package/dist/engine/paginate.d.ts.map +1 -0
- package/dist/engine/reflow.d.ts +22 -0
- package/dist/engine/reflow.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/use-header-footer.d.ts +10 -0
- package/dist/hooks/use-header-footer.d.ts.map +1 -0
- package/dist/hooks/use-overflow-detection.d.ts +11 -0
- package/dist/hooks/use-overflow-detection.d.ts.map +1 -0
- package/dist/hooks/use-pagination.d.ts +22 -0
- package/dist/hooks/use-pagination.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lex4-editor.cjs +3774 -0
- package/dist/lex4-editor.cjs.map +1 -0
- package/dist/lex4-editor.js +3774 -0
- package/dist/lex4-editor.js.map +1 -0
- package/dist/lexical/commands/format-commands.d.ts +14 -0
- package/dist/lexical/commands/format-commands.d.ts.map +1 -0
- package/dist/lexical/commands/index.d.ts +4 -0
- package/dist/lexical/commands/index.d.ts.map +1 -0
- package/dist/lexical/commands/list-commands.d.ts +25 -0
- package/dist/lexical/commands/list-commands.d.ts.map +1 -0
- package/dist/lexical/editor-setup.d.ts +10 -0
- package/dist/lexical/editor-setup.d.ts.map +1 -0
- package/dist/lexical/index.d.ts +8 -0
- package/dist/lexical/index.d.ts.map +1 -0
- package/dist/lexical/plugins/active-editor-plugin.d.ts +16 -0
- package/dist/lexical/plugins/active-editor-plugin.d.ts.map +1 -0
- package/dist/lexical/plugins/font-plugin.d.ts +19 -0
- package/dist/lexical/plugins/font-plugin.d.ts.map +1 -0
- package/dist/lexical/plugins/height-limit-plugin.d.ts +15 -0
- package/dist/lexical/plugins/height-limit-plugin.d.ts.map +1 -0
- package/dist/lexical/plugins/history-capture-plugin.d.ts +8 -0
- package/dist/lexical/plugins/history-capture-plugin.d.ts.map +1 -0
- package/dist/lexical/plugins/index.d.ts +9 -0
- package/dist/lexical/plugins/index.d.ts.map +1 -0
- package/dist/lexical/plugins/overflow-plugin.d.ts +22 -0
- package/dist/lexical/plugins/overflow-plugin.d.ts.map +1 -0
- package/dist/lexical/plugins/page-boundary-plugin.d.ts +9 -0
- package/dist/lexical/plugins/page-boundary-plugin.d.ts.map +1 -0
- package/dist/lexical/plugins/paragraph-indent-plugin.d.ts +7 -0
- package/dist/lexical/plugins/paragraph-indent-plugin.d.ts.map +1 -0
- package/dist/lexical/plugins/paste-plugin.d.ts +9 -0
- package/dist/lexical/plugins/paste-plugin.d.ts.map +1 -0
- package/dist/lexical/plugins/tab-indent-plugin.d.ts +9 -0
- package/dist/lexical/plugins/tab-indent-plugin.d.ts.map +1 -0
- package/dist/lexical/theme.d.ts +7 -0
- package/dist/lexical/theme.d.ts.map +1 -0
- package/dist/style.css +1065 -0
- package/dist/types/document.d.ts +40 -0
- package/dist/types/document.d.ts.map +1 -0
- package/dist/types/editor-props.d.ts +19 -0
- package/dist/types/editor-props.d.ts.map +1 -0
- package/dist/types/history.d.ts +43 -0
- package/dist/types/history.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/page.d.ts +26 -0
- package/dist/types/page.d.ts.map +1 -0
- package/dist/utils/debug.d.ts +41 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/editor-state-utils.d.ts +48 -0
- package/dist/utils/editor-state-utils.d.ts.map +1 -0
- package/dist/utils/history-manager.d.ts +26 -0
- package/dist/utils/history-manager.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,3774 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const React = require("react");
|
|
5
|
+
const lexical = require("lexical");
|
|
6
|
+
const LexicalComposerContext = require("@lexical/react/LexicalComposerContext");
|
|
7
|
+
const list = require("@lexical/list");
|
|
8
|
+
const LexicalComposer = require("@lexical/react/LexicalComposer");
|
|
9
|
+
const LexicalRichTextPlugin = require("@lexical/react/LexicalRichTextPlugin");
|
|
10
|
+
const LexicalContentEditable = require("@lexical/react/LexicalContentEditable");
|
|
11
|
+
const LexicalListPlugin = require("@lexical/react/LexicalListPlugin");
|
|
12
|
+
const LexicalErrorBoundary = require("@lexical/react/LexicalErrorBoundary");
|
|
13
|
+
const LexicalOnChangePlugin = require("@lexical/react/LexicalOnChangePlugin");
|
|
14
|
+
const richText = require("@lexical/rich-text");
|
|
15
|
+
function createEmptyPage(id) {
|
|
16
|
+
return {
|
|
17
|
+
id: id ?? crypto.randomUUID(),
|
|
18
|
+
bodyState: null,
|
|
19
|
+
headerState: null,
|
|
20
|
+
footerState: null,
|
|
21
|
+
headerHeight: 0,
|
|
22
|
+
footerHeight: 0,
|
|
23
|
+
bodySyncVersion: 0,
|
|
24
|
+
headerSyncVersion: 0,
|
|
25
|
+
footerSyncVersion: 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function createPageFromTemplate(template) {
|
|
29
|
+
return {
|
|
30
|
+
...createEmptyPage(),
|
|
31
|
+
headerState: (template == null ? void 0 : template.headerState) ?? null,
|
|
32
|
+
footerState: (template == null ? void 0 : template.footerState) ?? null,
|
|
33
|
+
headerHeight: (template == null ? void 0 : template.headerHeight) ?? 0,
|
|
34
|
+
footerHeight: (template == null ? void 0 : template.footerHeight) ?? 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function createEmptyDocument() {
|
|
38
|
+
return {
|
|
39
|
+
pages: [createEmptyPage()],
|
|
40
|
+
headerFooterEnabled: false,
|
|
41
|
+
pageCounterMode: "none",
|
|
42
|
+
defaultHeaderState: null,
|
|
43
|
+
defaultFooterState: null,
|
|
44
|
+
defaultHeaderHeight: 0,
|
|
45
|
+
defaultFooterHeight: 0
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const DocumentContext = React.createContext(null);
|
|
49
|
+
function useDocument() {
|
|
50
|
+
const ctx = React.useContext(DocumentContext);
|
|
51
|
+
if (!ctx) {
|
|
52
|
+
throw new Error("useDocument must be used within a DocumentProvider");
|
|
53
|
+
}
|
|
54
|
+
return ctx;
|
|
55
|
+
}
|
|
56
|
+
const A4_WIDTH_MM = 210;
|
|
57
|
+
const A4_HEIGHT_MM = 297;
|
|
58
|
+
const PX_PER_MM = 96 / 25.4;
|
|
59
|
+
const A4_WIDTH_PX = Math.round(A4_WIDTH_MM * PX_PER_MM);
|
|
60
|
+
const A4_HEIGHT_PX = Math.round(A4_HEIGHT_MM * PX_PER_MM);
|
|
61
|
+
const PAGE_MARGIN_PX = 40;
|
|
62
|
+
const MAX_HEADER_RATIO = 0.2;
|
|
63
|
+
const MAX_FOOTER_RATIO = 0.2;
|
|
64
|
+
const MAX_HEADER_HEIGHT_PX = Math.round(A4_HEIGHT_PX * MAX_HEADER_RATIO);
|
|
65
|
+
const MAX_FOOTER_HEIGHT_PX = Math.round(A4_HEIGHT_PX * MAX_FOOTER_RATIO);
|
|
66
|
+
const CHANNEL_COLORS = {
|
|
67
|
+
reducer: "#e91e63",
|
|
68
|
+
overflow: "#ff5722",
|
|
69
|
+
registry: "#9c27b0",
|
|
70
|
+
page: "#2196f3",
|
|
71
|
+
toolbar: "#4caf50",
|
|
72
|
+
header: "#ff9800",
|
|
73
|
+
footer: "#795548",
|
|
74
|
+
focus: "#607d8b"
|
|
75
|
+
};
|
|
76
|
+
function getFlag() {
|
|
77
|
+
if (typeof window === "undefined") return null;
|
|
78
|
+
const win = window.__LEX4_DEBUG__;
|
|
79
|
+
if (win !== void 0 && win !== null) return win;
|
|
80
|
+
if (typeof localStorage !== "undefined") {
|
|
81
|
+
return localStorage.getItem("lex4:debug");
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function isEnabled(channel) {
|
|
86
|
+
const flag = getFlag();
|
|
87
|
+
if (!flag) return false;
|
|
88
|
+
if (flag === true || flag === "*" || flag === "true") return true;
|
|
89
|
+
if (typeof flag === "string") {
|
|
90
|
+
return flag.split(",").some((s) => s.trim() === channel);
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
function debug(channel, message, ...data) {
|
|
95
|
+
if (!isEnabled(channel)) return;
|
|
96
|
+
const color = CHANNEL_COLORS[channel];
|
|
97
|
+
const prefix = `%c[lex4:${channel}]`;
|
|
98
|
+
const style = `color: ${color}; font-weight: bold;`;
|
|
99
|
+
if (data.length > 0) {
|
|
100
|
+
console.log(prefix, style, message, ...data);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(prefix, style, message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function debugWarn(channel, message, ...data) {
|
|
106
|
+
if (!isEnabled(channel)) return;
|
|
107
|
+
const color = CHANNEL_COLORS[channel];
|
|
108
|
+
const prefix = `%c[lex4:${channel}]`;
|
|
109
|
+
const style = `color: ${color}; font-weight: bold;`;
|
|
110
|
+
if (data.length > 0) {
|
|
111
|
+
console.warn(prefix, style, message, ...data);
|
|
112
|
+
} else {
|
|
113
|
+
console.warn(prefix, style, message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function shortId(id) {
|
|
117
|
+
return id.substring(0, 8);
|
|
118
|
+
}
|
|
119
|
+
function serializedStateChanged(current, next) {
|
|
120
|
+
return JSON.stringify(current) !== JSON.stringify(next);
|
|
121
|
+
}
|
|
122
|
+
function withExternalSyncVersions(currentDocument, nextDocument) {
|
|
123
|
+
const currentPages = new Map(currentDocument.pages.map((page) => [page.id, page]));
|
|
124
|
+
return {
|
|
125
|
+
...nextDocument,
|
|
126
|
+
pages: nextDocument.pages.map((nextPage) => {
|
|
127
|
+
const currentPage = currentPages.get(nextPage.id);
|
|
128
|
+
if (!currentPage) {
|
|
129
|
+
return nextPage;
|
|
130
|
+
}
|
|
131
|
+
const bodyChanged = serializedStateChanged(currentPage.bodyState, nextPage.bodyState);
|
|
132
|
+
const headerChanged = currentPage.headerHeight !== nextPage.headerHeight || serializedStateChanged(currentPage.headerState, nextPage.headerState);
|
|
133
|
+
const footerChanged = currentPage.footerHeight !== nextPage.footerHeight || serializedStateChanged(currentPage.footerState, nextPage.footerState);
|
|
134
|
+
return {
|
|
135
|
+
...nextPage,
|
|
136
|
+
bodySyncVersion: bodyChanged ? currentPage.bodySyncVersion + 1 : currentPage.bodySyncVersion,
|
|
137
|
+
headerSyncVersion: headerChanged ? currentPage.headerSyncVersion + 1 : currentPage.headerSyncVersion,
|
|
138
|
+
footerSyncVersion: footerChanged ? currentPage.footerSyncVersion + 1 : currentPage.footerSyncVersion
|
|
139
|
+
};
|
|
140
|
+
})
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function documentReducer(state, action) {
|
|
144
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
145
|
+
switch (action.type) {
|
|
146
|
+
case "ADD_PAGE": {
|
|
147
|
+
const newPage = action.page ?? createEmptyPage();
|
|
148
|
+
const pages = [...state.pages];
|
|
149
|
+
const insertAt = action.afterIndex !== void 0 ? action.afterIndex + 1 : pages.length;
|
|
150
|
+
pages.splice(insertAt, 0, newPage);
|
|
151
|
+
const hasBody = !!((_d = (_c = (_b = (_a = action.page) == null ? void 0 : _a.bodyState) == null ? void 0 : _b.root) == null ? void 0 : _c.children) == null ? void 0 : _d.length);
|
|
152
|
+
debug("reducer", `ADD_PAGE id=${shortId(newPage.id)} at=${insertAt} withBody=${hasBody} totalPages=${pages.length}`);
|
|
153
|
+
return { ...state, pages };
|
|
154
|
+
}
|
|
155
|
+
case "REMOVE_PAGE": {
|
|
156
|
+
if (state.pages.length <= 1) {
|
|
157
|
+
debug("reducer", `REMOVE_PAGE id=${shortId(action.pageId)} — skipped (last page)`);
|
|
158
|
+
return state;
|
|
159
|
+
}
|
|
160
|
+
debug("reducer", `REMOVE_PAGE id=${shortId(action.pageId)} remaining=${state.pages.length - 1}`);
|
|
161
|
+
return {
|
|
162
|
+
...state,
|
|
163
|
+
pages: state.pages.filter((p) => p.id !== action.pageId)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
case "UPDATE_PAGE_BODY": {
|
|
167
|
+
const childCount = ((_g = (_f = (_e = action.bodyState) == null ? void 0 : _e.root) == null ? void 0 : _f.children) == null ? void 0 : _g.length) ?? 0;
|
|
168
|
+
debug("reducer", `UPDATE_PAGE_BODY id=${shortId(action.pageId)} children=${childCount}`);
|
|
169
|
+
return {
|
|
170
|
+
...state,
|
|
171
|
+
pages: state.pages.map(
|
|
172
|
+
(p) => p.id === action.pageId ? { ...p, bodyState: action.bodyState } : p
|
|
173
|
+
)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
case "UPDATE_PAGE_HEADER": {
|
|
177
|
+
debug("reducer", `UPDATE_PAGE_HEADER id=${shortId(action.pageId)}`);
|
|
178
|
+
return {
|
|
179
|
+
...state,
|
|
180
|
+
pages: state.pages.map(
|
|
181
|
+
(p) => p.id === action.pageId ? { ...p, headerState: action.headerState } : p
|
|
182
|
+
)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
case "UPDATE_PAGE_HEADER_CONTENT": {
|
|
186
|
+
const clamped = Math.min(action.height, MAX_HEADER_HEIGHT_PX);
|
|
187
|
+
debug("reducer", `UPDATE_PAGE_HEADER_CONTENT id=${shortId(action.pageId)} height=${clamped}`);
|
|
188
|
+
return {
|
|
189
|
+
...state,
|
|
190
|
+
pages: state.pages.map(
|
|
191
|
+
(p) => p.id === action.pageId ? { ...p, headerState: action.headerState, headerHeight: clamped } : p
|
|
192
|
+
)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
case "UPDATE_PAGE_FOOTER": {
|
|
196
|
+
debug("reducer", `UPDATE_PAGE_FOOTER id=${shortId(action.pageId)}`);
|
|
197
|
+
return {
|
|
198
|
+
...state,
|
|
199
|
+
pages: state.pages.map(
|
|
200
|
+
(p) => p.id === action.pageId ? { ...p, footerState: action.footerState } : p
|
|
201
|
+
)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
case "UPDATE_PAGE_FOOTER_CONTENT": {
|
|
205
|
+
const clamped = Math.min(action.height, MAX_FOOTER_HEIGHT_PX);
|
|
206
|
+
debug("reducer", `UPDATE_PAGE_FOOTER_CONTENT id=${shortId(action.pageId)} height=${clamped}`);
|
|
207
|
+
return {
|
|
208
|
+
...state,
|
|
209
|
+
pages: state.pages.map(
|
|
210
|
+
(p) => p.id === action.pageId ? { ...p, footerState: action.footerState, footerHeight: clamped } : p
|
|
211
|
+
)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
case "SET_HEADER_FOOTER_ENABLED": {
|
|
215
|
+
debug("reducer", `SET_HEADER_FOOTER_ENABLED enabled=${action.enabled}`);
|
|
216
|
+
return { ...state, headerFooterEnabled: action.enabled };
|
|
217
|
+
}
|
|
218
|
+
case "SET_PAGE_COUNTER_MODE": {
|
|
219
|
+
debug("reducer", `SET_PAGE_COUNTER_MODE mode=${action.mode}`);
|
|
220
|
+
return { ...state, pageCounterMode: action.mode };
|
|
221
|
+
}
|
|
222
|
+
case "COPY_HEADER_TO_ALL": {
|
|
223
|
+
debug("reducer", `COPY_HEADER_TO_ALL from=${shortId(action.sourcePageId)}`);
|
|
224
|
+
const source = state.pages.find((p) => p.id === action.sourcePageId);
|
|
225
|
+
if (!source) return state;
|
|
226
|
+
return {
|
|
227
|
+
...state,
|
|
228
|
+
defaultHeaderState: source.headerState,
|
|
229
|
+
defaultHeaderHeight: source.headerHeight,
|
|
230
|
+
pages: state.pages.map((p) => ({
|
|
231
|
+
...p,
|
|
232
|
+
headerState: source.headerState,
|
|
233
|
+
headerHeight: source.headerHeight,
|
|
234
|
+
headerSyncVersion: p.headerSyncVersion + 1
|
|
235
|
+
}))
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
case "COPY_FOOTER_TO_ALL": {
|
|
239
|
+
debug("reducer", `COPY_FOOTER_TO_ALL from=${shortId(action.sourcePageId)}`);
|
|
240
|
+
const source = state.pages.find((p) => p.id === action.sourcePageId);
|
|
241
|
+
if (!source) return state;
|
|
242
|
+
return {
|
|
243
|
+
...state,
|
|
244
|
+
defaultFooterState: source.footerState,
|
|
245
|
+
defaultFooterHeight: source.footerHeight,
|
|
246
|
+
pages: state.pages.map((p) => ({
|
|
247
|
+
...p,
|
|
248
|
+
footerState: source.footerState,
|
|
249
|
+
footerHeight: source.footerHeight,
|
|
250
|
+
footerSyncVersion: p.footerSyncVersion + 1
|
|
251
|
+
}))
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
case "CLEAR_HEADER": {
|
|
255
|
+
debug("reducer", `CLEAR_HEADER id=${shortId(action.pageId)}`);
|
|
256
|
+
return {
|
|
257
|
+
...state,
|
|
258
|
+
pages: state.pages.map(
|
|
259
|
+
(p) => p.id === action.pageId ? { ...p, headerState: null, headerHeight: 0, headerSyncVersion: p.headerSyncVersion + 1 } : p
|
|
260
|
+
)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
case "CLEAR_FOOTER": {
|
|
264
|
+
debug("reducer", `CLEAR_FOOTER id=${shortId(action.pageId)}`);
|
|
265
|
+
return {
|
|
266
|
+
...state,
|
|
267
|
+
pages: state.pages.map(
|
|
268
|
+
(p) => p.id === action.pageId ? { ...p, footerState: null, footerHeight: 0, footerSyncVersion: p.footerSyncVersion + 1 } : p
|
|
269
|
+
)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
case "CLEAR_ALL_HEADERS": {
|
|
273
|
+
debug("reducer", "CLEAR_ALL_HEADERS");
|
|
274
|
+
return {
|
|
275
|
+
...state,
|
|
276
|
+
defaultHeaderState: null,
|
|
277
|
+
defaultHeaderHeight: 0,
|
|
278
|
+
pages: state.pages.map((p) => ({
|
|
279
|
+
...p,
|
|
280
|
+
headerState: null,
|
|
281
|
+
headerHeight: 0,
|
|
282
|
+
headerSyncVersion: p.headerSyncVersion + 1
|
|
283
|
+
}))
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
case "CLEAR_ALL_FOOTERS": {
|
|
287
|
+
debug("reducer", "CLEAR_ALL_FOOTERS");
|
|
288
|
+
return {
|
|
289
|
+
...state,
|
|
290
|
+
defaultFooterState: null,
|
|
291
|
+
defaultFooterHeight: 0,
|
|
292
|
+
pages: state.pages.map((p) => ({
|
|
293
|
+
...p,
|
|
294
|
+
footerState: null,
|
|
295
|
+
footerHeight: 0,
|
|
296
|
+
footerSyncVersion: p.footerSyncVersion + 1
|
|
297
|
+
}))
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
case "CLEAR_DOCUMENT_CONTENT": {
|
|
301
|
+
debug("reducer", "CLEAR_DOCUMENT_CONTENT");
|
|
302
|
+
const firstPage = state.pages[0];
|
|
303
|
+
return {
|
|
304
|
+
...state,
|
|
305
|
+
pages: [{
|
|
306
|
+
...createEmptyPage(firstPage == null ? void 0 : firstPage.id),
|
|
307
|
+
headerState: (firstPage == null ? void 0 : firstPage.headerState) ?? null,
|
|
308
|
+
footerState: (firstPage == null ? void 0 : firstPage.footerState) ?? null,
|
|
309
|
+
headerHeight: (firstPage == null ? void 0 : firstPage.headerHeight) ?? 0,
|
|
310
|
+
footerHeight: (firstPage == null ? void 0 : firstPage.footerHeight) ?? 0,
|
|
311
|
+
bodySyncVersion: ((firstPage == null ? void 0 : firstPage.bodySyncVersion) ?? 0) + 1,
|
|
312
|
+
headerSyncVersion: (firstPage == null ? void 0 : firstPage.headerSyncVersion) ?? 0,
|
|
313
|
+
footerSyncVersion: (firstPage == null ? void 0 : firstPage.footerSyncVersion) ?? 0
|
|
314
|
+
}]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
case "SET_HEADER_HEIGHT": {
|
|
318
|
+
const clamped = Math.min(action.height, MAX_HEADER_HEIGHT_PX);
|
|
319
|
+
debug("header", `SET_HEADER_HEIGHT id=${shortId(action.pageId)} raw=${action.height} clamped=${clamped}`);
|
|
320
|
+
return {
|
|
321
|
+
...state,
|
|
322
|
+
pages: state.pages.map(
|
|
323
|
+
(p) => p.id === action.pageId ? { ...p, headerHeight: clamped } : p
|
|
324
|
+
)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
case "SET_FOOTER_HEIGHT": {
|
|
328
|
+
const clamped = Math.min(action.height, MAX_FOOTER_HEIGHT_PX);
|
|
329
|
+
debug("footer", `SET_FOOTER_HEIGHT id=${shortId(action.pageId)} raw=${action.height} clamped=${clamped}`);
|
|
330
|
+
return {
|
|
331
|
+
...state,
|
|
332
|
+
pages: state.pages.map(
|
|
333
|
+
(p) => p.id === action.pageId ? { ...p, footerHeight: clamped } : p
|
|
334
|
+
)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
case "SET_DOCUMENT": {
|
|
338
|
+
debug("reducer", `SET_DOCUMENT pages=${action.document.pages.length}`);
|
|
339
|
+
return withExternalSyncVersions(state, action.document);
|
|
340
|
+
}
|
|
341
|
+
default:
|
|
342
|
+
return state;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const MAX_HISTORY_ACTIONS = 100;
|
|
346
|
+
function cloneSnapshot(document2) {
|
|
347
|
+
return structuredClone(document2);
|
|
348
|
+
}
|
|
349
|
+
function createHistoryEntry(document2, descriptor, undoSnapshot, caretPosition, caretSelection, undoCaretPosition, undoCaretSelection) {
|
|
350
|
+
return {
|
|
351
|
+
...descriptor,
|
|
352
|
+
id: crypto.randomUUID(),
|
|
353
|
+
snapshot: cloneSnapshot(document2),
|
|
354
|
+
undoSnapshot: cloneSnapshot(undoSnapshot),
|
|
355
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
356
|
+
caretPosition,
|
|
357
|
+
caretSelection,
|
|
358
|
+
undoCaretPosition,
|
|
359
|
+
undoCaretSelection
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function createHistoryState(document2, baseCaretPosition = null) {
|
|
363
|
+
return {
|
|
364
|
+
baseSnapshot: cloneSnapshot(document2),
|
|
365
|
+
baseCaretPosition,
|
|
366
|
+
entries: [],
|
|
367
|
+
cursor: 0
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function getHistorySnapshot(history, cursor = history.cursor) {
|
|
371
|
+
if (cursor <= 0) {
|
|
372
|
+
return cloneSnapshot(history.baseSnapshot);
|
|
373
|
+
}
|
|
374
|
+
return cloneSnapshot(history.entries[cursor - 1].snapshot);
|
|
375
|
+
}
|
|
376
|
+
function clearHistoryState(document2, baseCaretPosition = null) {
|
|
377
|
+
return createHistoryState(document2, baseCaretPosition);
|
|
378
|
+
}
|
|
379
|
+
function recordHistoryEntry(history, nextDocument, descriptor, undoSnapshot, caretPosition, caretSelection, undoCaretPosition, undoCaretSelection) {
|
|
380
|
+
const entries = history.entries.slice(0, history.cursor);
|
|
381
|
+
entries.push(createHistoryEntry(
|
|
382
|
+
nextDocument,
|
|
383
|
+
descriptor,
|
|
384
|
+
undoSnapshot,
|
|
385
|
+
caretPosition,
|
|
386
|
+
caretSelection,
|
|
387
|
+
undoCaretPosition,
|
|
388
|
+
undoCaretSelection
|
|
389
|
+
));
|
|
390
|
+
let baseSnapshot = cloneSnapshot(history.baseSnapshot);
|
|
391
|
+
let baseCaretPosition = history.baseCaretPosition;
|
|
392
|
+
let trimmedEntries = entries;
|
|
393
|
+
if (trimmedEntries.length > MAX_HISTORY_ACTIONS) {
|
|
394
|
+
baseSnapshot = cloneSnapshot(trimmedEntries[1].undoSnapshot);
|
|
395
|
+
baseCaretPosition = trimmedEntries[1].undoCaretPosition;
|
|
396
|
+
trimmedEntries = trimmedEntries.slice(1);
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
baseSnapshot,
|
|
400
|
+
baseCaretPosition,
|
|
401
|
+
entries: trimmedEntries,
|
|
402
|
+
cursor: trimmedEntries.length
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function undoHistory(history) {
|
|
406
|
+
if (history.cursor === 0) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const cursor = history.cursor - 1;
|
|
410
|
+
const nextHistory = {
|
|
411
|
+
...history,
|
|
412
|
+
cursor
|
|
413
|
+
};
|
|
414
|
+
return {
|
|
415
|
+
history: nextHistory,
|
|
416
|
+
document: cloneSnapshot(history.entries[history.cursor - 1].undoSnapshot),
|
|
417
|
+
caretPosition: history.entries[history.cursor - 1].undoCaretPosition,
|
|
418
|
+
caretSelection: history.entries[history.cursor - 1].undoCaretSelection
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function redoHistory(history) {
|
|
422
|
+
var _a, _b;
|
|
423
|
+
if (history.cursor >= history.entries.length) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
const cursor = history.cursor + 1;
|
|
427
|
+
const nextHistory = {
|
|
428
|
+
...history,
|
|
429
|
+
cursor
|
|
430
|
+
};
|
|
431
|
+
return {
|
|
432
|
+
history: nextHistory,
|
|
433
|
+
document: getHistorySnapshot(nextHistory),
|
|
434
|
+
caretPosition: ((_a = nextHistory.entries[cursor - 1]) == null ? void 0 : _a.caretPosition) ?? nextHistory.baseCaretPosition,
|
|
435
|
+
caretSelection: ((_b = nextHistory.entries[cursor - 1]) == null ? void 0 : _b.caretSelection) ?? null
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function jumpToHistoryEntry(history, entryIndex) {
|
|
439
|
+
var _a, _b;
|
|
440
|
+
if (entryIndex < 0 || entryIndex >= history.entries.length) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
const nextHistory = {
|
|
444
|
+
...history,
|
|
445
|
+
cursor: entryIndex + 1
|
|
446
|
+
};
|
|
447
|
+
return {
|
|
448
|
+
history: nextHistory,
|
|
449
|
+
document: getHistorySnapshot(nextHistory),
|
|
450
|
+
caretPosition: ((_a = nextHistory.entries[nextHistory.cursor - 1]) == null ? void 0 : _a.caretPosition) ?? nextHistory.baseCaretPosition,
|
|
451
|
+
caretSelection: ((_b = nextHistory.entries[nextHistory.cursor - 1]) == null ? void 0 : _b.caretSelection) ?? null
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const HISTORY_RESTORE_SUPPRESSION_MS = 100;
|
|
455
|
+
const HISTORY_BATCH_FLUSH_MS = 16;
|
|
456
|
+
function cloneDocumentSnapshot(document2) {
|
|
457
|
+
return structuredClone(document2);
|
|
458
|
+
}
|
|
459
|
+
function captureUndoSnapshot(document2, activeEditor, caretPosition) {
|
|
460
|
+
const snapshot = cloneDocumentSnapshot(document2);
|
|
461
|
+
if (!activeEditor || !caretPosition) {
|
|
462
|
+
return snapshot;
|
|
463
|
+
}
|
|
464
|
+
const page = snapshot.pages.find((candidate) => candidate.id === caretPosition.pageId);
|
|
465
|
+
if (!page) {
|
|
466
|
+
return snapshot;
|
|
467
|
+
}
|
|
468
|
+
const editorState = activeEditor.getEditorState().toJSON();
|
|
469
|
+
if (caretPosition.region === "body") {
|
|
470
|
+
page.bodyState = editorState;
|
|
471
|
+
} else if (caretPosition.region === "header") {
|
|
472
|
+
page.headerState = editorState;
|
|
473
|
+
} else {
|
|
474
|
+
page.footerState = editorState;
|
|
475
|
+
}
|
|
476
|
+
return snapshot;
|
|
477
|
+
}
|
|
478
|
+
function captureCaretSelection(editor) {
|
|
479
|
+
if (!editor) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
const rootElement = editor.getRootElement();
|
|
483
|
+
const domSelection = window.getSelection();
|
|
484
|
+
const selectionInRoot = (node) => !!node && !!rootElement && (node === rootElement || rootElement.contains(node));
|
|
485
|
+
const getTextOffset = (node, offset) => {
|
|
486
|
+
if (!rootElement) {
|
|
487
|
+
return 0;
|
|
488
|
+
}
|
|
489
|
+
const range = document.createRange();
|
|
490
|
+
range.setStart(rootElement, 0);
|
|
491
|
+
range.setEnd(node, offset);
|
|
492
|
+
return range.toString().length;
|
|
493
|
+
};
|
|
494
|
+
let caretSelection = null;
|
|
495
|
+
editor.getEditorState().read(() => {
|
|
496
|
+
const selection = lexical.$createRangeSelectionFromDom(window.getSelection(), editor) ?? lexical.$getSelection();
|
|
497
|
+
if (!lexical.$isRangeSelection(selection)) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
caretSelection = {
|
|
501
|
+
anchor: {
|
|
502
|
+
key: selection.anchor.key,
|
|
503
|
+
offset: selection.anchor.offset,
|
|
504
|
+
type: selection.anchor.type
|
|
505
|
+
},
|
|
506
|
+
focus: {
|
|
507
|
+
key: selection.focus.key,
|
|
508
|
+
offset: selection.focus.offset,
|
|
509
|
+
type: selection.focus.type
|
|
510
|
+
},
|
|
511
|
+
anchorTextOffset: domSelection && selectionInRoot(domSelection.anchorNode) ? getTextOffset(domSelection.anchorNode, domSelection.anchorOffset) : 0,
|
|
512
|
+
focusTextOffset: domSelection && selectionInRoot(domSelection.focusNode) ? getTextOffset(domSelection.focusNode, domSelection.focusOffset) : 0,
|
|
513
|
+
format: selection.format,
|
|
514
|
+
style: selection.style
|
|
515
|
+
};
|
|
516
|
+
});
|
|
517
|
+
return caretSelection;
|
|
518
|
+
}
|
|
519
|
+
function getPageNumber(document2, pageId) {
|
|
520
|
+
if (!pageId) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const index = document2.pages.findIndex((page) => page.id === pageId);
|
|
524
|
+
return index >= 0 ? index + 1 : null;
|
|
525
|
+
}
|
|
526
|
+
function describeAction(action, document2) {
|
|
527
|
+
switch (action.type) {
|
|
528
|
+
case "UPDATE_PAGE_BODY": {
|
|
529
|
+
const pageNumber = getPageNumber(document2, action.pageId);
|
|
530
|
+
return {
|
|
531
|
+
label: pageNumber ? `Edited body - Page ${pageNumber}` : "Edited body",
|
|
532
|
+
source: "body",
|
|
533
|
+
pageId: action.pageId,
|
|
534
|
+
region: "body"
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
case "UPDATE_PAGE_HEADER":
|
|
538
|
+
case "UPDATE_PAGE_HEADER_CONTENT": {
|
|
539
|
+
const pageNumber = getPageNumber(document2, action.pageId);
|
|
540
|
+
return {
|
|
541
|
+
label: pageNumber ? `Edited header - Page ${pageNumber}` : "Edited header",
|
|
542
|
+
source: "header",
|
|
543
|
+
pageId: action.pageId,
|
|
544
|
+
region: "header"
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
case "UPDATE_PAGE_FOOTER":
|
|
548
|
+
case "UPDATE_PAGE_FOOTER_CONTENT": {
|
|
549
|
+
const pageNumber = getPageNumber(document2, action.pageId);
|
|
550
|
+
return {
|
|
551
|
+
label: pageNumber ? `Edited footer - Page ${pageNumber}` : "Edited footer",
|
|
552
|
+
source: "footer",
|
|
553
|
+
pageId: action.pageId,
|
|
554
|
+
region: "footer"
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
case "SET_HEADER_FOOTER_ENABLED":
|
|
558
|
+
return {
|
|
559
|
+
label: action.enabled ? "Enabled headers and footers" : "Disabled headers and footers",
|
|
560
|
+
source: "document",
|
|
561
|
+
region: "document"
|
|
562
|
+
};
|
|
563
|
+
case "SET_PAGE_COUNTER_MODE":
|
|
564
|
+
return {
|
|
565
|
+
label: `Page counter set to ${action.mode}`,
|
|
566
|
+
source: "document",
|
|
567
|
+
region: "document"
|
|
568
|
+
};
|
|
569
|
+
case "COPY_HEADER_TO_ALL":
|
|
570
|
+
return {
|
|
571
|
+
label: "Copied header to all pages",
|
|
572
|
+
source: "toolbar",
|
|
573
|
+
pageId: action.sourcePageId,
|
|
574
|
+
region: "header"
|
|
575
|
+
};
|
|
576
|
+
case "COPY_FOOTER_TO_ALL":
|
|
577
|
+
return {
|
|
578
|
+
label: "Copied footer to all pages",
|
|
579
|
+
source: "toolbar",
|
|
580
|
+
pageId: action.sourcePageId,
|
|
581
|
+
region: "footer"
|
|
582
|
+
};
|
|
583
|
+
case "CLEAR_HEADER": {
|
|
584
|
+
const pageNumber = getPageNumber(document2, action.pageId);
|
|
585
|
+
return {
|
|
586
|
+
label: pageNumber ? `Cleared header - Page ${pageNumber}` : "Cleared header",
|
|
587
|
+
source: "toolbar",
|
|
588
|
+
pageId: action.pageId,
|
|
589
|
+
region: "header"
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
case "CLEAR_FOOTER": {
|
|
593
|
+
const pageNumber = getPageNumber(document2, action.pageId);
|
|
594
|
+
return {
|
|
595
|
+
label: pageNumber ? `Cleared footer - Page ${pageNumber}` : "Cleared footer",
|
|
596
|
+
source: "toolbar",
|
|
597
|
+
pageId: action.pageId,
|
|
598
|
+
region: "footer"
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
case "CLEAR_ALL_HEADERS":
|
|
602
|
+
return {
|
|
603
|
+
label: "Cleared all headers",
|
|
604
|
+
source: "toolbar",
|
|
605
|
+
region: "header"
|
|
606
|
+
};
|
|
607
|
+
case "CLEAR_ALL_FOOTERS":
|
|
608
|
+
return {
|
|
609
|
+
label: "Cleared all footers",
|
|
610
|
+
source: "toolbar",
|
|
611
|
+
region: "footer"
|
|
612
|
+
};
|
|
613
|
+
case "CLEAR_DOCUMENT_CONTENT":
|
|
614
|
+
return {
|
|
615
|
+
label: "Cleared document body",
|
|
616
|
+
source: "document",
|
|
617
|
+
region: "document"
|
|
618
|
+
};
|
|
619
|
+
case "SET_HEADER_HEIGHT": {
|
|
620
|
+
const pageNumber = getPageNumber(document2, action.pageId);
|
|
621
|
+
return {
|
|
622
|
+
label: pageNumber ? `Resized header - Page ${pageNumber}` : "Resized header",
|
|
623
|
+
source: "header",
|
|
624
|
+
pageId: action.pageId,
|
|
625
|
+
region: "header"
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
case "SET_FOOTER_HEIGHT": {
|
|
629
|
+
const pageNumber = getPageNumber(document2, action.pageId);
|
|
630
|
+
return {
|
|
631
|
+
label: pageNumber ? `Resized footer - Page ${pageNumber}` : "Resized footer",
|
|
632
|
+
source: "footer",
|
|
633
|
+
pageId: action.pageId,
|
|
634
|
+
region: "footer"
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
case "ADD_PAGE":
|
|
638
|
+
return {
|
|
639
|
+
label: "Added page",
|
|
640
|
+
source: "overflow",
|
|
641
|
+
region: "document"
|
|
642
|
+
};
|
|
643
|
+
case "REMOVE_PAGE":
|
|
644
|
+
return {
|
|
645
|
+
label: "Removed page",
|
|
646
|
+
source: "overflow",
|
|
647
|
+
region: "document"
|
|
648
|
+
};
|
|
649
|
+
case "SET_DOCUMENT":
|
|
650
|
+
return {
|
|
651
|
+
label: "Document reflow",
|
|
652
|
+
source: "overflow",
|
|
653
|
+
region: "document"
|
|
654
|
+
};
|
|
655
|
+
default:
|
|
656
|
+
return {
|
|
657
|
+
label: "Updated document",
|
|
658
|
+
source: "document",
|
|
659
|
+
region: "document"
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function documentsAreEqual(current, next) {
|
|
664
|
+
return JSON.stringify(current) === JSON.stringify(next);
|
|
665
|
+
}
|
|
666
|
+
const DocumentProvider = ({
|
|
667
|
+
initialDocument,
|
|
668
|
+
onDocumentChange,
|
|
669
|
+
children
|
|
670
|
+
}) => {
|
|
671
|
+
var _a, _b;
|
|
672
|
+
const initialSnapshot = initialDocument ?? createEmptyDocument();
|
|
673
|
+
const [document2, baseDispatch] = React.useReducer(documentReducer, initialSnapshot);
|
|
674
|
+
const [historyState, setHistoryState] = React.useState(() => createHistoryState(initialSnapshot));
|
|
675
|
+
const documentRef = React.useRef(document2);
|
|
676
|
+
const historyStateRef = React.useRef(historyState);
|
|
677
|
+
const [activePageId, setActivePageIdRaw] = React.useState(
|
|
678
|
+
((_a = initialSnapshot.pages[0]) == null ? void 0 : _a.id) ?? null
|
|
679
|
+
);
|
|
680
|
+
const activePageIdRef = React.useRef(((_b = initialSnapshot.pages[0]) == null ? void 0 : _b.id) ?? null);
|
|
681
|
+
const [globalSelectionActive, setGlobalSelectionActive] = React.useState(false);
|
|
682
|
+
const [historySidebarOpen, setHistorySidebarOpen] = React.useState(true);
|
|
683
|
+
const [focusAtEndVersion, setFocusAtEndVersion] = React.useState(0);
|
|
684
|
+
const activeEditorRef = React.useRef(null);
|
|
685
|
+
const activeCaretPositionRef = React.useRef(null);
|
|
686
|
+
const pendingCaretPositionRef = React.useRef(null);
|
|
687
|
+
const pendingCaretSelectionRef = React.useRef(null);
|
|
688
|
+
const pendingFocusAtEndRef = React.useRef(null);
|
|
689
|
+
const [, forceUpdate] = React.useState(0);
|
|
690
|
+
const historySuppressedRef = React.useRef(false);
|
|
691
|
+
const historyReleaseTimerRef = React.useRef(null);
|
|
692
|
+
const historyFlushTimerRef = React.useRef(null);
|
|
693
|
+
const pendingHistoryActionRef = React.useRef(null);
|
|
694
|
+
const pendingUndoSnapshotRef = React.useRef(null);
|
|
695
|
+
const pendingUndoCaretPositionRef = React.useRef(null);
|
|
696
|
+
const pendingUndoCaretSelectionRef = React.useRef(null);
|
|
697
|
+
const historyBatchRef = React.useRef(null);
|
|
698
|
+
const editorMapRef = React.useRef(/* @__PURE__ */ new Map());
|
|
699
|
+
const editorRegistry = React.useMemo(() => ({
|
|
700
|
+
register: (pageId, editor) => {
|
|
701
|
+
editorMapRef.current.set(pageId, editor);
|
|
702
|
+
},
|
|
703
|
+
unregister: (pageId) => {
|
|
704
|
+
editorMapRef.current.delete(pageId);
|
|
705
|
+
},
|
|
706
|
+
get: (pageId) => editorMapRef.current.get(pageId),
|
|
707
|
+
all: () => Array.from(editorMapRef.current.values())
|
|
708
|
+
}), []);
|
|
709
|
+
const setActivePageId = React.useCallback((id) => {
|
|
710
|
+
activePageIdRef.current = id;
|
|
711
|
+
setActivePageIdRaw(id);
|
|
712
|
+
}, []);
|
|
713
|
+
const setActiveEditor = React.useCallback((editor, caretPosition = null) => {
|
|
714
|
+
activeEditorRef.current = editor;
|
|
715
|
+
activeCaretPositionRef.current = caretPosition;
|
|
716
|
+
forceUpdate((n) => n + 1);
|
|
717
|
+
}, []);
|
|
718
|
+
const focusEditorAtEnd = React.useCallback((editor, caretPosition) => {
|
|
719
|
+
requestAnimationFrame(() => {
|
|
720
|
+
setActivePageId(caretPosition.pageId);
|
|
721
|
+
setActiveEditor(editor, caretPosition);
|
|
722
|
+
editor.focus(() => {
|
|
723
|
+
editor.update(() => {
|
|
724
|
+
lexical.$getRoot().selectEnd();
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
}, [setActiveEditor, setActivePageId]);
|
|
729
|
+
const consumePendingCaretPosition = React.useCallback((caretPosition) => {
|
|
730
|
+
const pendingCaretPosition = pendingCaretPositionRef.current;
|
|
731
|
+
if (!pendingCaretPosition || pendingCaretPosition.pageId !== caretPosition.pageId || pendingCaretPosition.region !== caretPosition.region) {
|
|
732
|
+
return void 0;
|
|
733
|
+
}
|
|
734
|
+
pendingCaretPositionRef.current = null;
|
|
735
|
+
const pendingCaretSelection = pendingCaretSelectionRef.current;
|
|
736
|
+
pendingCaretSelectionRef.current = null;
|
|
737
|
+
return pendingCaretSelection;
|
|
738
|
+
}, []);
|
|
739
|
+
const consumePendingFocusAtEnd = React.useCallback((caretPosition) => {
|
|
740
|
+
const pendingFocusAtEnd = pendingFocusAtEndRef.current;
|
|
741
|
+
if (!pendingFocusAtEnd || pendingFocusAtEnd.pageId !== caretPosition.pageId || pendingFocusAtEnd.region !== caretPosition.region) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
pendingFocusAtEndRef.current = null;
|
|
745
|
+
return true;
|
|
746
|
+
}, []);
|
|
747
|
+
const requestFocusAtEnd = React.useCallback((caretPosition) => {
|
|
748
|
+
pendingCaretPositionRef.current = null;
|
|
749
|
+
pendingCaretSelectionRef.current = null;
|
|
750
|
+
if (caretPosition.region === "body") {
|
|
751
|
+
const editor = editorRegistry.get(caretPosition.pageId);
|
|
752
|
+
if (editor) {
|
|
753
|
+
pendingFocusAtEndRef.current = null;
|
|
754
|
+
focusEditorAtEnd(editor, caretPosition);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
pendingFocusAtEndRef.current = caretPosition;
|
|
759
|
+
setFocusAtEndVersion((version) => version + 1);
|
|
760
|
+
}, [editorRegistry, focusEditorAtEnd]);
|
|
761
|
+
const flushHistoryBatch = React.useCallback(() => {
|
|
762
|
+
if (historyFlushTimerRef.current) {
|
|
763
|
+
clearTimeout(historyFlushTimerRef.current);
|
|
764
|
+
historyFlushTimerRef.current = null;
|
|
765
|
+
}
|
|
766
|
+
const batch = historyBatchRef.current;
|
|
767
|
+
historyBatchRef.current = null;
|
|
768
|
+
pendingHistoryActionRef.current = null;
|
|
769
|
+
pendingUndoSnapshotRef.current = null;
|
|
770
|
+
pendingUndoCaretPositionRef.current = null;
|
|
771
|
+
pendingUndoCaretSelectionRef.current = null;
|
|
772
|
+
if (!batch || historySuppressedRef.current || !batch.hasMutation || !batch.nextDocument) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const undoSnapshot = batch.undoSnapshot ?? batch.nextDocument;
|
|
776
|
+
setHistoryState((previousHistory) => {
|
|
777
|
+
const nextHistory = recordHistoryEntry(
|
|
778
|
+
previousHistory,
|
|
779
|
+
batch.nextDocument,
|
|
780
|
+
batch.descriptor,
|
|
781
|
+
undoSnapshot,
|
|
782
|
+
activeCaretPositionRef.current,
|
|
783
|
+
captureCaretSelection(activeEditorRef.current),
|
|
784
|
+
batch.undoCaretPosition,
|
|
785
|
+
batch.undoCaretSelection
|
|
786
|
+
);
|
|
787
|
+
historyStateRef.current = nextHistory;
|
|
788
|
+
return nextHistory;
|
|
789
|
+
});
|
|
790
|
+
}, []);
|
|
791
|
+
const scheduleHistoryBatchFlush = React.useCallback(() => {
|
|
792
|
+
if (historyFlushTimerRef.current) {
|
|
793
|
+
clearTimeout(historyFlushTimerRef.current);
|
|
794
|
+
}
|
|
795
|
+
historyFlushTimerRef.current = setTimeout(() => {
|
|
796
|
+
flushHistoryBatch();
|
|
797
|
+
}, HISTORY_BATCH_FLUSH_MS);
|
|
798
|
+
}, [flushHistoryBatch]);
|
|
799
|
+
const queueHistoryAction = React.useCallback((action) => {
|
|
800
|
+
pendingHistoryActionRef.current = action;
|
|
801
|
+
pendingUndoSnapshotRef.current = action ? captureUndoSnapshot(
|
|
802
|
+
documentRef.current,
|
|
803
|
+
activeEditorRef.current,
|
|
804
|
+
activeCaretPositionRef.current
|
|
805
|
+
) : null;
|
|
806
|
+
pendingUndoCaretPositionRef.current = action ? activeCaretPositionRef.current : null;
|
|
807
|
+
pendingUndoCaretSelectionRef.current = action ? captureCaretSelection(activeEditorRef.current) : null;
|
|
808
|
+
}, []);
|
|
809
|
+
const runHistoryAction = React.useCallback((action, callback) => {
|
|
810
|
+
if (historyBatchRef.current) {
|
|
811
|
+
callback();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
historyBatchRef.current = {
|
|
815
|
+
descriptor: action,
|
|
816
|
+
nextDocument: null,
|
|
817
|
+
undoSnapshot: captureUndoSnapshot(
|
|
818
|
+
documentRef.current,
|
|
819
|
+
activeEditorRef.current,
|
|
820
|
+
activeCaretPositionRef.current
|
|
821
|
+
),
|
|
822
|
+
undoCaretPosition: activeCaretPositionRef.current,
|
|
823
|
+
undoCaretSelection: captureCaretSelection(activeEditorRef.current),
|
|
824
|
+
hasMutation: false
|
|
825
|
+
};
|
|
826
|
+
pendingHistoryActionRef.current = action;
|
|
827
|
+
pendingUndoSnapshotRef.current = null;
|
|
828
|
+
pendingUndoCaretPositionRef.current = null;
|
|
829
|
+
pendingUndoCaretSelectionRef.current = null;
|
|
830
|
+
try {
|
|
831
|
+
callback();
|
|
832
|
+
} finally {
|
|
833
|
+
scheduleHistoryBatchFlush();
|
|
834
|
+
}
|
|
835
|
+
}, [scheduleHistoryBatchFlush]);
|
|
836
|
+
const suppressHistoryTemporarily = React.useCallback(() => {
|
|
837
|
+
historySuppressedRef.current = true;
|
|
838
|
+
pendingHistoryActionRef.current = null;
|
|
839
|
+
pendingUndoSnapshotRef.current = null;
|
|
840
|
+
pendingUndoCaretPositionRef.current = null;
|
|
841
|
+
pendingUndoCaretSelectionRef.current = null;
|
|
842
|
+
historyBatchRef.current = null;
|
|
843
|
+
if (historyFlushTimerRef.current) {
|
|
844
|
+
clearTimeout(historyFlushTimerRef.current);
|
|
845
|
+
historyFlushTimerRef.current = null;
|
|
846
|
+
}
|
|
847
|
+
if (historyReleaseTimerRef.current) {
|
|
848
|
+
clearTimeout(historyReleaseTimerRef.current);
|
|
849
|
+
}
|
|
850
|
+
historyReleaseTimerRef.current = setTimeout(() => {
|
|
851
|
+
historySuppressedRef.current = false;
|
|
852
|
+
historyReleaseTimerRef.current = null;
|
|
853
|
+
}, HISTORY_RESTORE_SUPPRESSION_MS);
|
|
854
|
+
}, []);
|
|
855
|
+
const dispatch = React.useCallback((action) => {
|
|
856
|
+
const currentDocument = documentRef.current;
|
|
857
|
+
const nextDocument = documentReducer(currentDocument, action);
|
|
858
|
+
const changed = !documentsAreEqual(currentDocument, nextDocument);
|
|
859
|
+
const suppressPassiveBodyHistory = action.type === "UPDATE_PAGE_BODY" && action.pageId !== activePageIdRef.current && !historyBatchRef.current && !pendingHistoryActionRef.current;
|
|
860
|
+
documentRef.current = nextDocument;
|
|
861
|
+
if (changed && !historySuppressedRef.current && !suppressPassiveBodyHistory) {
|
|
862
|
+
if (historyBatchRef.current) {
|
|
863
|
+
historyBatchRef.current.nextDocument = cloneDocumentSnapshot(nextDocument);
|
|
864
|
+
historyBatchRef.current.hasMutation = true;
|
|
865
|
+
scheduleHistoryBatchFlush();
|
|
866
|
+
} else if (pendingHistoryActionRef.current) {
|
|
867
|
+
historyBatchRef.current = {
|
|
868
|
+
descriptor: pendingHistoryActionRef.current,
|
|
869
|
+
nextDocument: cloneDocumentSnapshot(nextDocument),
|
|
870
|
+
undoSnapshot: pendingUndoSnapshotRef.current,
|
|
871
|
+
undoCaretPosition: pendingUndoCaretPositionRef.current,
|
|
872
|
+
undoCaretSelection: pendingUndoCaretSelectionRef.current,
|
|
873
|
+
hasMutation: true
|
|
874
|
+
};
|
|
875
|
+
scheduleHistoryBatchFlush();
|
|
876
|
+
} else {
|
|
877
|
+
const descriptor = describeAction(action, currentDocument);
|
|
878
|
+
setHistoryState((previousHistory) => {
|
|
879
|
+
const nextHistory = recordHistoryEntry(
|
|
880
|
+
previousHistory,
|
|
881
|
+
nextDocument,
|
|
882
|
+
descriptor,
|
|
883
|
+
captureUndoSnapshot(
|
|
884
|
+
currentDocument,
|
|
885
|
+
activeEditorRef.current,
|
|
886
|
+
activeCaretPositionRef.current
|
|
887
|
+
),
|
|
888
|
+
activeCaretPositionRef.current,
|
|
889
|
+
captureCaretSelection(activeEditorRef.current),
|
|
890
|
+
activeCaretPositionRef.current,
|
|
891
|
+
captureCaretSelection(activeEditorRef.current)
|
|
892
|
+
);
|
|
893
|
+
historyStateRef.current = nextHistory;
|
|
894
|
+
return nextHistory;
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
pendingHistoryActionRef.current = null;
|
|
899
|
+
pendingUndoSnapshotRef.current = null;
|
|
900
|
+
pendingUndoCaretPositionRef.current = null;
|
|
901
|
+
pendingUndoCaretSelectionRef.current = null;
|
|
902
|
+
}
|
|
903
|
+
baseDispatch(action);
|
|
904
|
+
}, [scheduleHistoryBatchFlush]);
|
|
905
|
+
const restoreDocument = React.useCallback((snapshot, caretPosition, caretSelection) => {
|
|
906
|
+
suppressHistoryTemporarily();
|
|
907
|
+
pendingCaretPositionRef.current = caretPosition;
|
|
908
|
+
pendingCaretSelectionRef.current = caretSelection;
|
|
909
|
+
activeEditorRef.current = null;
|
|
910
|
+
activeCaretPositionRef.current = caretPosition;
|
|
911
|
+
forceUpdate((n) => n + 1);
|
|
912
|
+
documentRef.current = cloneDocumentSnapshot(snapshot);
|
|
913
|
+
baseDispatch({ type: "SET_DOCUMENT", document: cloneDocumentSnapshot(snapshot) });
|
|
914
|
+
}, [suppressHistoryTemporarily]);
|
|
915
|
+
const undo = React.useCallback(() => {
|
|
916
|
+
flushHistoryBatch();
|
|
917
|
+
const result = undoHistory(historyStateRef.current);
|
|
918
|
+
if (!result) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
historyStateRef.current = result.history;
|
|
922
|
+
setHistoryState(result.history);
|
|
923
|
+
restoreDocument(result.document, result.caretPosition, result.caretSelection);
|
|
924
|
+
}, [flushHistoryBatch, restoreDocument]);
|
|
925
|
+
const redo = React.useCallback(() => {
|
|
926
|
+
flushHistoryBatch();
|
|
927
|
+
const result = redoHistory(historyStateRef.current);
|
|
928
|
+
if (!result) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
historyStateRef.current = result.history;
|
|
932
|
+
setHistoryState(result.history);
|
|
933
|
+
restoreDocument(result.document, result.caretPosition, result.caretSelection);
|
|
934
|
+
}, [flushHistoryBatch, restoreDocument]);
|
|
935
|
+
const clearHistory = React.useCallback(() => {
|
|
936
|
+
flushHistoryBatch();
|
|
937
|
+
const nextHistory = clearHistoryState(documentRef.current, activeCaretPositionRef.current);
|
|
938
|
+
historyStateRef.current = nextHistory;
|
|
939
|
+
setHistoryState(nextHistory);
|
|
940
|
+
}, [flushHistoryBatch]);
|
|
941
|
+
const handleJumpToHistoryEntry = React.useCallback((entryIndex) => {
|
|
942
|
+
flushHistoryBatch();
|
|
943
|
+
const result = jumpToHistoryEntry(historyStateRef.current, entryIndex);
|
|
944
|
+
if (!result) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
historyStateRef.current = result.history;
|
|
948
|
+
setHistoryState(result.history);
|
|
949
|
+
restoreDocument(result.document, result.caretPosition, result.caretSelection);
|
|
950
|
+
}, [flushHistoryBatch, restoreDocument]);
|
|
951
|
+
React.useEffect(() => {
|
|
952
|
+
documentRef.current = document2;
|
|
953
|
+
onDocumentChange == null ? void 0 : onDocumentChange(document2);
|
|
954
|
+
}, [document2, onDocumentChange]);
|
|
955
|
+
React.useEffect(() => {
|
|
956
|
+
historyStateRef.current = historyState;
|
|
957
|
+
}, [historyState]);
|
|
958
|
+
React.useEffect(() => {
|
|
959
|
+
var _a2;
|
|
960
|
+
const firstPageId = ((_a2 = document2.pages[0]) == null ? void 0 : _a2.id) ?? null;
|
|
961
|
+
const activePageStillExists = activePageId !== null && document2.pages.some((page) => page.id === activePageId);
|
|
962
|
+
if (!activePageStillExists) {
|
|
963
|
+
activePageIdRef.current = firstPageId;
|
|
964
|
+
setActivePageIdRaw(firstPageId);
|
|
965
|
+
activeEditorRef.current = null;
|
|
966
|
+
activeCaretPositionRef.current = null;
|
|
967
|
+
forceUpdate((n) => n + 1);
|
|
968
|
+
}
|
|
969
|
+
}, [activePageId, document2.pages]);
|
|
970
|
+
React.useEffect(
|
|
971
|
+
() => () => {
|
|
972
|
+
if (historyFlushTimerRef.current) {
|
|
973
|
+
clearTimeout(historyFlushTimerRef.current);
|
|
974
|
+
}
|
|
975
|
+
if (historyReleaseTimerRef.current) {
|
|
976
|
+
clearTimeout(historyReleaseTimerRef.current);
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
[]
|
|
980
|
+
);
|
|
981
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DocumentContext.Provider, { value: {
|
|
982
|
+
document: document2,
|
|
983
|
+
dispatch,
|
|
984
|
+
activePageId,
|
|
985
|
+
setActivePageId,
|
|
986
|
+
activeEditor: activeEditorRef.current,
|
|
987
|
+
consumePendingCaretPosition,
|
|
988
|
+
consumePendingFocusAtEnd,
|
|
989
|
+
requestFocusAtEnd,
|
|
990
|
+
focusAtEndVersion,
|
|
991
|
+
setActiveEditor,
|
|
992
|
+
globalSelectionActive,
|
|
993
|
+
setGlobalSelectionActive,
|
|
994
|
+
historyEntries: historyState.entries,
|
|
995
|
+
historyCursor: historyState.cursor,
|
|
996
|
+
canUndo: historyState.cursor > 0,
|
|
997
|
+
canRedo: historyState.cursor < historyState.entries.length,
|
|
998
|
+
queueHistoryAction,
|
|
999
|
+
runHistoryAction,
|
|
1000
|
+
jumpToHistoryEntry: handleJumpToHistoryEntry,
|
|
1001
|
+
clearHistory,
|
|
1002
|
+
historySidebarOpen,
|
|
1003
|
+
setHistorySidebarOpen,
|
|
1004
|
+
undo,
|
|
1005
|
+
redo,
|
|
1006
|
+
editorRegistry
|
|
1007
|
+
}, children });
|
|
1008
|
+
};
|
|
1009
|
+
/**
|
|
1010
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1011
|
+
*
|
|
1012
|
+
* This source code is licensed under the ISC license.
|
|
1013
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1014
|
+
*/
|
|
1015
|
+
const mergeClasses = (...classes) => classes.filter((className, index, array) => {
|
|
1016
|
+
return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
|
|
1017
|
+
}).join(" ").trim();
|
|
1018
|
+
/**
|
|
1019
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1020
|
+
*
|
|
1021
|
+
* This source code is licensed under the ISC license.
|
|
1022
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1023
|
+
*/
|
|
1024
|
+
const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1025
|
+
/**
|
|
1026
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1027
|
+
*
|
|
1028
|
+
* This source code is licensed under the ISC license.
|
|
1029
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1030
|
+
*/
|
|
1031
|
+
const toCamelCase = (string) => string.replace(
|
|
1032
|
+
/^([A-Z])|[\s-_]+(\w)/g,
|
|
1033
|
+
(match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase()
|
|
1034
|
+
);
|
|
1035
|
+
/**
|
|
1036
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1037
|
+
*
|
|
1038
|
+
* This source code is licensed under the ISC license.
|
|
1039
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1040
|
+
*/
|
|
1041
|
+
const toPascalCase = (string) => {
|
|
1042
|
+
const camelCase = toCamelCase(string);
|
|
1043
|
+
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
|
|
1044
|
+
};
|
|
1045
|
+
/**
|
|
1046
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1047
|
+
*
|
|
1048
|
+
* This source code is licensed under the ISC license.
|
|
1049
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1050
|
+
*/
|
|
1051
|
+
var defaultAttributes = {
|
|
1052
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1053
|
+
width: 24,
|
|
1054
|
+
height: 24,
|
|
1055
|
+
viewBox: "0 0 24 24",
|
|
1056
|
+
fill: "none",
|
|
1057
|
+
stroke: "currentColor",
|
|
1058
|
+
strokeWidth: 2,
|
|
1059
|
+
strokeLinecap: "round",
|
|
1060
|
+
strokeLinejoin: "round"
|
|
1061
|
+
};
|
|
1062
|
+
/**
|
|
1063
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1064
|
+
*
|
|
1065
|
+
* This source code is licensed under the ISC license.
|
|
1066
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1067
|
+
*/
|
|
1068
|
+
const hasA11yProp = (props) => {
|
|
1069
|
+
for (const prop in props) {
|
|
1070
|
+
if (prop.startsWith("aria-") || prop === "role" || prop === "title") {
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return false;
|
|
1075
|
+
};
|
|
1076
|
+
const LucideContext = React.createContext({});
|
|
1077
|
+
const useLucideContext = () => React.useContext(LucideContext);
|
|
1078
|
+
const Icon = React.forwardRef(
|
|
1079
|
+
({ color, size, strokeWidth, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => {
|
|
1080
|
+
const {
|
|
1081
|
+
size: contextSize = 24,
|
|
1082
|
+
strokeWidth: contextStrokeWidth = 2,
|
|
1083
|
+
absoluteStrokeWidth: contextAbsoluteStrokeWidth = false,
|
|
1084
|
+
color: contextColor = "currentColor",
|
|
1085
|
+
className: contextClass = ""
|
|
1086
|
+
} = useLucideContext() ?? {};
|
|
1087
|
+
const calculatedStrokeWidth = absoluteStrokeWidth ?? contextAbsoluteStrokeWidth ? Number(strokeWidth ?? contextStrokeWidth) * 24 / Number(size ?? contextSize) : strokeWidth ?? contextStrokeWidth;
|
|
1088
|
+
return React.createElement(
|
|
1089
|
+
"svg",
|
|
1090
|
+
{
|
|
1091
|
+
ref,
|
|
1092
|
+
...defaultAttributes,
|
|
1093
|
+
width: size ?? contextSize ?? defaultAttributes.width,
|
|
1094
|
+
height: size ?? contextSize ?? defaultAttributes.height,
|
|
1095
|
+
stroke: color ?? contextColor,
|
|
1096
|
+
strokeWidth: calculatedStrokeWidth,
|
|
1097
|
+
className: mergeClasses("lucide", contextClass, className),
|
|
1098
|
+
...!children && !hasA11yProp(rest) && { "aria-hidden": "true" },
|
|
1099
|
+
...rest
|
|
1100
|
+
},
|
|
1101
|
+
[
|
|
1102
|
+
...iconNode.map(([tag, attrs]) => React.createElement(tag, attrs)),
|
|
1103
|
+
...Array.isArray(children) ? children : [children]
|
|
1104
|
+
]
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
);
|
|
1108
|
+
/**
|
|
1109
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1110
|
+
*
|
|
1111
|
+
* This source code is licensed under the ISC license.
|
|
1112
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1113
|
+
*/
|
|
1114
|
+
const createLucideIcon = (iconName, iconNode) => {
|
|
1115
|
+
const Component = React.forwardRef(
|
|
1116
|
+
({ className, ...props }, ref) => React.createElement(Icon, {
|
|
1117
|
+
ref,
|
|
1118
|
+
iconNode,
|
|
1119
|
+
className: mergeClasses(
|
|
1120
|
+
`lucide-${toKebabCase(toPascalCase(iconName))}`,
|
|
1121
|
+
`lucide-${iconName}`,
|
|
1122
|
+
className
|
|
1123
|
+
),
|
|
1124
|
+
...props
|
|
1125
|
+
})
|
|
1126
|
+
);
|
|
1127
|
+
Component.displayName = toPascalCase(iconName);
|
|
1128
|
+
return Component;
|
|
1129
|
+
};
|
|
1130
|
+
/**
|
|
1131
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1132
|
+
*
|
|
1133
|
+
* This source code is licensed under the ISC license.
|
|
1134
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1135
|
+
*/
|
|
1136
|
+
const __iconNode$j = [
|
|
1137
|
+
[
|
|
1138
|
+
"path",
|
|
1139
|
+
{ d: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8", key: "mg9rjx" }
|
|
1140
|
+
]
|
|
1141
|
+
];
|
|
1142
|
+
const Bold = createLucideIcon("bold", __iconNode$j);
|
|
1143
|
+
/**
|
|
1144
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1145
|
+
*
|
|
1146
|
+
* This source code is licensed under the ISC license.
|
|
1147
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1148
|
+
*/
|
|
1149
|
+
const __iconNode$i = [
|
|
1150
|
+
["line", { x1: "15", x2: "15", y1: "12", y2: "18", key: "1p7wdc" }],
|
|
1151
|
+
["line", { x1: "12", x2: "18", y1: "15", y2: "15", key: "1nscbv" }],
|
|
1152
|
+
["rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2", key: "17jyea" }],
|
|
1153
|
+
["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2", key: "zix9uf" }]
|
|
1154
|
+
];
|
|
1155
|
+
const CopyPlus = createLucideIcon("copy-plus", __iconNode$i);
|
|
1156
|
+
/**
|
|
1157
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1158
|
+
*
|
|
1159
|
+
* This source code is licensed under the ISC license.
|
|
1160
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1161
|
+
*/
|
|
1162
|
+
const __iconNode$h = [
|
|
1163
|
+
[
|
|
1164
|
+
"path",
|
|
1165
|
+
{
|
|
1166
|
+
d: "M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21",
|
|
1167
|
+
key: "g5wo59"
|
|
1168
|
+
}
|
|
1169
|
+
],
|
|
1170
|
+
["path", { d: "m5.082 11.09 8.828 8.828", key: "1wx5vj" }]
|
|
1171
|
+
];
|
|
1172
|
+
const Eraser = createLucideIcon("eraser", __iconNode$h);
|
|
1173
|
+
/**
|
|
1174
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1175
|
+
*
|
|
1176
|
+
* This source code is licensed under the ISC license.
|
|
1177
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1178
|
+
*/
|
|
1179
|
+
const __iconNode$g = [
|
|
1180
|
+
["line", { x1: "4", x2: "20", y1: "9", y2: "9", key: "4lhtct" }],
|
|
1181
|
+
["line", { x1: "4", x2: "20", y1: "15", y2: "15", key: "vyu0kd" }],
|
|
1182
|
+
["line", { x1: "10", x2: "8", y1: "3", y2: "21", key: "1ggp8o" }],
|
|
1183
|
+
["line", { x1: "16", x2: "14", y1: "3", y2: "21", key: "weycgp" }]
|
|
1184
|
+
];
|
|
1185
|
+
const Hash = createLucideIcon("hash", __iconNode$g);
|
|
1186
|
+
/**
|
|
1187
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1188
|
+
*
|
|
1189
|
+
* This source code is licensed under the ISC license.
|
|
1190
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1191
|
+
*/
|
|
1192
|
+
const __iconNode$f = [
|
|
1193
|
+
["line", { x1: "19", x2: "10", y1: "4", y2: "4", key: "15jd3p" }],
|
|
1194
|
+
["line", { x1: "14", x2: "5", y1: "20", y2: "20", key: "bu0au3" }],
|
|
1195
|
+
["line", { x1: "15", x2: "9", y1: "4", y2: "20", key: "uljnxc" }]
|
|
1196
|
+
];
|
|
1197
|
+
const Italic = createLucideIcon("italic", __iconNode$f);
|
|
1198
|
+
/**
|
|
1199
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1200
|
+
*
|
|
1201
|
+
* This source code is licensed under the ISC license.
|
|
1202
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1203
|
+
*/
|
|
1204
|
+
const __iconNode$e = [
|
|
1205
|
+
["path", { d: "M21 5H11", key: "us1j55" }],
|
|
1206
|
+
["path", { d: "M21 12H11", key: "wd7e0v" }],
|
|
1207
|
+
["path", { d: "M21 19H11", key: "saa85w" }],
|
|
1208
|
+
["path", { d: "m7 8-4 4 4 4", key: "o5hrat" }]
|
|
1209
|
+
];
|
|
1210
|
+
const ListIndentDecrease = createLucideIcon("list-indent-decrease", __iconNode$e);
|
|
1211
|
+
/**
|
|
1212
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1213
|
+
*
|
|
1214
|
+
* This source code is licensed under the ISC license.
|
|
1215
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1216
|
+
*/
|
|
1217
|
+
const __iconNode$d = [
|
|
1218
|
+
["path", { d: "M21 5H11", key: "us1j55" }],
|
|
1219
|
+
["path", { d: "M21 12H11", key: "wd7e0v" }],
|
|
1220
|
+
["path", { d: "M21 19H11", key: "saa85w" }],
|
|
1221
|
+
["path", { d: "m3 8 4 4-4 4", key: "1a3j6y" }]
|
|
1222
|
+
];
|
|
1223
|
+
const ListIndentIncrease = createLucideIcon("list-indent-increase", __iconNode$d);
|
|
1224
|
+
/**
|
|
1225
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1226
|
+
*
|
|
1227
|
+
* This source code is licensed under the ISC license.
|
|
1228
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1229
|
+
*/
|
|
1230
|
+
const __iconNode$c = [
|
|
1231
|
+
["path", { d: "M11 5h10", key: "1cz7ny" }],
|
|
1232
|
+
["path", { d: "M11 12h10", key: "1438ji" }],
|
|
1233
|
+
["path", { d: "M11 19h10", key: "11t30w" }],
|
|
1234
|
+
["path", { d: "M4 4h1v5", key: "10yrso" }],
|
|
1235
|
+
["path", { d: "M4 9h2", key: "r1h2o0" }],
|
|
1236
|
+
["path", { d: "M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02", key: "xtkcd5" }]
|
|
1237
|
+
];
|
|
1238
|
+
const ListOrdered = createLucideIcon("list-ordered", __iconNode$c);
|
|
1239
|
+
/**
|
|
1240
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1241
|
+
*
|
|
1242
|
+
* This source code is licensed under the ISC license.
|
|
1243
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1244
|
+
*/
|
|
1245
|
+
const __iconNode$b = [
|
|
1246
|
+
["path", { d: "M3 5h.01", key: "18ugdj" }],
|
|
1247
|
+
["path", { d: "M3 12h.01", key: "nlz23k" }],
|
|
1248
|
+
["path", { d: "M3 19h.01", key: "noohij" }],
|
|
1249
|
+
["path", { d: "M8 5h13", key: "1pao27" }],
|
|
1250
|
+
["path", { d: "M8 12h13", key: "1za7za" }],
|
|
1251
|
+
["path", { d: "M8 19h13", key: "m83p4d" }]
|
|
1252
|
+
];
|
|
1253
|
+
const List = createLucideIcon("list", __iconNode$b);
|
|
1254
|
+
/**
|
|
1255
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1256
|
+
*
|
|
1257
|
+
* This source code is licensed under the ISC license.
|
|
1258
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1259
|
+
*/
|
|
1260
|
+
const __iconNode$a = [
|
|
1261
|
+
["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }],
|
|
1262
|
+
["path", { d: "M15 3v18", key: "14nvp0" }]
|
|
1263
|
+
];
|
|
1264
|
+
const PanelRight = createLucideIcon("panel-right", __iconNode$a);
|
|
1265
|
+
/**
|
|
1266
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1267
|
+
*
|
|
1268
|
+
* This source code is licensed under the ISC license.
|
|
1269
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1270
|
+
*/
|
|
1271
|
+
const __iconNode$9 = [
|
|
1272
|
+
["path", { d: "m15 14 5-5-5-5", key: "12vg1m" }],
|
|
1273
|
+
["path", { d: "M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13", key: "6uklza" }]
|
|
1274
|
+
];
|
|
1275
|
+
const Redo2 = createLucideIcon("redo-2", __iconNode$9);
|
|
1276
|
+
/**
|
|
1277
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1278
|
+
*
|
|
1279
|
+
* This source code is licensed under the ISC license.
|
|
1280
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1281
|
+
*/
|
|
1282
|
+
const __iconNode$8 = [
|
|
1283
|
+
["path", { d: "M16 4H9a3 3 0 0 0-2.83 4", key: "43sutm" }],
|
|
1284
|
+
["path", { d: "M14 12a4 4 0 0 1 0 8H6", key: "nlfj13" }],
|
|
1285
|
+
["line", { x1: "4", x2: "20", y1: "12", y2: "12", key: "1e0a9i" }]
|
|
1286
|
+
];
|
|
1287
|
+
const Strikethrough = createLucideIcon("strikethrough", __iconNode$8);
|
|
1288
|
+
/**
|
|
1289
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1290
|
+
*
|
|
1291
|
+
* This source code is licensed under the ISC license.
|
|
1292
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1293
|
+
*/
|
|
1294
|
+
const __iconNode$7 = [
|
|
1295
|
+
["path", { d: "M21 5H3", key: "1fi0y6" }],
|
|
1296
|
+
["path", { d: "M17 12H7", key: "16if0g" }],
|
|
1297
|
+
["path", { d: "M19 19H5", key: "vjpgq2" }]
|
|
1298
|
+
];
|
|
1299
|
+
const TextAlignCenter = createLucideIcon("text-align-center", __iconNode$7);
|
|
1300
|
+
/**
|
|
1301
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1302
|
+
*
|
|
1303
|
+
* This source code is licensed under the ISC license.
|
|
1304
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1305
|
+
*/
|
|
1306
|
+
const __iconNode$6 = [
|
|
1307
|
+
["path", { d: "M21 5H3", key: "1fi0y6" }],
|
|
1308
|
+
["path", { d: "M21 12H9", key: "dn1m92" }],
|
|
1309
|
+
["path", { d: "M21 19H7", key: "4cu937" }]
|
|
1310
|
+
];
|
|
1311
|
+
const TextAlignEnd = createLucideIcon("text-align-end", __iconNode$6);
|
|
1312
|
+
/**
|
|
1313
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1314
|
+
*
|
|
1315
|
+
* This source code is licensed under the ISC license.
|
|
1316
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1317
|
+
*/
|
|
1318
|
+
const __iconNode$5 = [
|
|
1319
|
+
["path", { d: "M3 5h18", key: "1u36vt" }],
|
|
1320
|
+
["path", { d: "M3 12h18", key: "1i2n21" }],
|
|
1321
|
+
["path", { d: "M3 19h18", key: "awlh7x" }]
|
|
1322
|
+
];
|
|
1323
|
+
const TextAlignJustify = createLucideIcon("text-align-justify", __iconNode$5);
|
|
1324
|
+
/**
|
|
1325
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1326
|
+
*
|
|
1327
|
+
* This source code is licensed under the ISC license.
|
|
1328
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1329
|
+
*/
|
|
1330
|
+
const __iconNode$4 = [
|
|
1331
|
+
["path", { d: "M21 5H3", key: "1fi0y6" }],
|
|
1332
|
+
["path", { d: "M15 12H3", key: "6jk70r" }],
|
|
1333
|
+
["path", { d: "M17 19H3", key: "z6ezky" }]
|
|
1334
|
+
];
|
|
1335
|
+
const TextAlignStart = createLucideIcon("text-align-start", __iconNode$4);
|
|
1336
|
+
/**
|
|
1337
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1338
|
+
*
|
|
1339
|
+
* This source code is licensed under the ISC license.
|
|
1340
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1341
|
+
*/
|
|
1342
|
+
const __iconNode$3 = [
|
|
1343
|
+
["path", { d: "M10 11v6", key: "nco0om" }],
|
|
1344
|
+
["path", { d: "M14 11v6", key: "outv1u" }],
|
|
1345
|
+
["path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6", key: "miytrc" }],
|
|
1346
|
+
["path", { d: "M3 6h18", key: "d0wm0j" }],
|
|
1347
|
+
["path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2", key: "e791ji" }]
|
|
1348
|
+
];
|
|
1349
|
+
const Trash2 = createLucideIcon("trash-2", __iconNode$3);
|
|
1350
|
+
/**
|
|
1351
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1352
|
+
*
|
|
1353
|
+
* This source code is licensed under the ISC license.
|
|
1354
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1355
|
+
*/
|
|
1356
|
+
const __iconNode$2 = [
|
|
1357
|
+
["path", { d: "M6 4v6a6 6 0 0 0 12 0V4", key: "9kb039" }],
|
|
1358
|
+
["line", { x1: "4", x2: "20", y1: "20", y2: "20", key: "nun2al" }]
|
|
1359
|
+
];
|
|
1360
|
+
const Underline = createLucideIcon("underline", __iconNode$2);
|
|
1361
|
+
/**
|
|
1362
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1363
|
+
*
|
|
1364
|
+
* This source code is licensed under the ISC license.
|
|
1365
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1366
|
+
*/
|
|
1367
|
+
const __iconNode$1 = [
|
|
1368
|
+
["path", { d: "M9 14 4 9l5-5", key: "102s5s" }],
|
|
1369
|
+
["path", { d: "M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11", key: "f3b9sd" }]
|
|
1370
|
+
];
|
|
1371
|
+
const Undo2 = createLucideIcon("undo-2", __iconNode$1);
|
|
1372
|
+
/**
|
|
1373
|
+
* @license lucide-react v1.8.0 - ISC
|
|
1374
|
+
*
|
|
1375
|
+
* This source code is licensed under the ISC license.
|
|
1376
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1377
|
+
*/
|
|
1378
|
+
const __iconNode = [
|
|
1379
|
+
["path", { d: "M18 6 6 18", key: "1bl5f8" }],
|
|
1380
|
+
["path", { d: "m6 6 12 12", key: "d8bk6v" }]
|
|
1381
|
+
];
|
|
1382
|
+
const X = createLucideIcon("x", __iconNode);
|
|
1383
|
+
const EditorSidebar = ({
|
|
1384
|
+
title,
|
|
1385
|
+
subtitle,
|
|
1386
|
+
open,
|
|
1387
|
+
onClose,
|
|
1388
|
+
headerActions,
|
|
1389
|
+
children
|
|
1390
|
+
}) => {
|
|
1391
|
+
if (!open) {
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1395
|
+
"aside",
|
|
1396
|
+
{
|
|
1397
|
+
className: "flex h-full w-[320px] shrink-0 flex-col border-l border-gray-200 bg-white",
|
|
1398
|
+
"data-testid": "editor-sidebar",
|
|
1399
|
+
children: [
|
|
1400
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between border-b border-gray-200 px-3 py-2.5", children: [
|
|
1401
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1402
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xs font-semibold text-gray-900", children: title }),
|
|
1403
|
+
subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: subtitle })
|
|
1404
|
+
] }),
|
|
1405
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1406
|
+
headerActions,
|
|
1407
|
+
onClose && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1408
|
+
"button",
|
|
1409
|
+
{
|
|
1410
|
+
type: "button",
|
|
1411
|
+
className: "flex h-6 w-6 items-center justify-center rounded text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600",
|
|
1412
|
+
"data-testid": "close-editor-sidebar",
|
|
1413
|
+
onClick: onClose,
|
|
1414
|
+
"aria-label": "Close sidebar",
|
|
1415
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(X, { size: 14 })
|
|
1416
|
+
}
|
|
1417
|
+
)
|
|
1418
|
+
] })
|
|
1419
|
+
] }),
|
|
1420
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-auto", children })
|
|
1421
|
+
]
|
|
1422
|
+
}
|
|
1423
|
+
);
|
|
1424
|
+
};
|
|
1425
|
+
function formatTimestamp(timestamp) {
|
|
1426
|
+
const date = new Date(timestamp);
|
|
1427
|
+
return date.toLocaleTimeString([], {
|
|
1428
|
+
hour: "2-digit",
|
|
1429
|
+
minute: "2-digit",
|
|
1430
|
+
second: "2-digit"
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
const HistorySidebar = () => {
|
|
1434
|
+
const {
|
|
1435
|
+
clearHistory,
|
|
1436
|
+
historyEntries,
|
|
1437
|
+
historyCursor,
|
|
1438
|
+
historySidebarOpen,
|
|
1439
|
+
jumpToHistoryEntry: jumpToHistoryEntry2,
|
|
1440
|
+
setHistorySidebarOpen
|
|
1441
|
+
} = useDocument();
|
|
1442
|
+
const visibleEntries = historyEntries.slice().reverse();
|
|
1443
|
+
const headerActions = /* @__PURE__ */ jsxRuntime.jsx(
|
|
1444
|
+
"button",
|
|
1445
|
+
{
|
|
1446
|
+
type: "button",
|
|
1447
|
+
title: "Clear History",
|
|
1448
|
+
"aria-label": "Clear History",
|
|
1449
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1450
|
+
onClick: () => clearHistory("manual"),
|
|
1451
|
+
className: "flex h-6 w-6 items-center justify-center rounded text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600",
|
|
1452
|
+
"data-testid": "clear-history",
|
|
1453
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { size: 13 })
|
|
1454
|
+
}
|
|
1455
|
+
);
|
|
1456
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1457
|
+
EditorSidebar,
|
|
1458
|
+
{
|
|
1459
|
+
title: "History",
|
|
1460
|
+
subtitle: "Word-style session history (last 100 actions)",
|
|
1461
|
+
open: historySidebarOpen,
|
|
1462
|
+
onClose: () => setHistorySidebarOpen(false),
|
|
1463
|
+
headerActions,
|
|
1464
|
+
children: visibleEntries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1465
|
+
"div",
|
|
1466
|
+
{
|
|
1467
|
+
className: "px-3 py-4 text-xs text-gray-500",
|
|
1468
|
+
"data-testid": "history-empty",
|
|
1469
|
+
children: "No history yet."
|
|
1470
|
+
}
|
|
1471
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "divide-y divide-gray-100", "data-testid": "history-entry-list", children: visibleEntries.map((entry, reversedIndex) => {
|
|
1472
|
+
const actualIndex = historyEntries.length - reversedIndex - 1;
|
|
1473
|
+
const isCurrent = actualIndex === historyCursor - 1;
|
|
1474
|
+
return /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1475
|
+
"button",
|
|
1476
|
+
{
|
|
1477
|
+
type: "button",
|
|
1478
|
+
className: `w-full px-3 py-2 text-left transition-colors ${isCurrent ? "bg-blue-50" : "bg-white hover:bg-gray-50"}`,
|
|
1479
|
+
"data-testid": `history-entry-${actualIndex}`,
|
|
1480
|
+
"data-history-current": isCurrent ? "true" : "false",
|
|
1481
|
+
onClick: () => jumpToHistoryEntry2(actualIndex),
|
|
1482
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
1483
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
|
|
1484
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `text-xs ${isCurrent ? "font-semibold text-blue-700" : "text-gray-900"}`, children: entry.label }),
|
|
1485
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-0.5 text-xs text-gray-400", children: entry.source })
|
|
1486
|
+
] }),
|
|
1487
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0 text-xs text-gray-400", children: formatTimestamp(entry.timestamp) })
|
|
1488
|
+
] })
|
|
1489
|
+
}
|
|
1490
|
+
) }, entry.id);
|
|
1491
|
+
}) })
|
|
1492
|
+
}
|
|
1493
|
+
);
|
|
1494
|
+
};
|
|
1495
|
+
const HeaderFooterToggle = ({
|
|
1496
|
+
enabled,
|
|
1497
|
+
onToggle
|
|
1498
|
+
}) => {
|
|
1499
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1500
|
+
"label",
|
|
1501
|
+
{
|
|
1502
|
+
className: "flex items-center gap-1.5 cursor-pointer select-none",
|
|
1503
|
+
"data-testid": "header-footer-toggle",
|
|
1504
|
+
children: [
|
|
1505
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-gray-600", children: "Headers & Footers" }),
|
|
1506
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1507
|
+
"button",
|
|
1508
|
+
{
|
|
1509
|
+
type: "button",
|
|
1510
|
+
role: "switch",
|
|
1511
|
+
"aria-checked": enabled,
|
|
1512
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1513
|
+
onClick: () => onToggle(!enabled),
|
|
1514
|
+
className: `
|
|
1515
|
+
relative inline-flex h-4 w-7 items-center rounded-full
|
|
1516
|
+
transition-colors duration-200
|
|
1517
|
+
${enabled ? "bg-blue-500" : "bg-gray-300"}
|
|
1518
|
+
`,
|
|
1519
|
+
"data-testid": "header-footer-switch",
|
|
1520
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1521
|
+
"span",
|
|
1522
|
+
{
|
|
1523
|
+
className: `
|
|
1524
|
+
inline-block h-3 w-3 rounded-full bg-white shadow-sm
|
|
1525
|
+
transition-transform duration-200
|
|
1526
|
+
${enabled ? "translate-x-3.5" : "translate-x-0.5"}
|
|
1527
|
+
`
|
|
1528
|
+
}
|
|
1529
|
+
)
|
|
1530
|
+
}
|
|
1531
|
+
)
|
|
1532
|
+
]
|
|
1533
|
+
}
|
|
1534
|
+
);
|
|
1535
|
+
};
|
|
1536
|
+
const HeaderFooterActions = ({
|
|
1537
|
+
activePageId,
|
|
1538
|
+
pageCounterMode,
|
|
1539
|
+
onPageCounterModeChange,
|
|
1540
|
+
onCopyHeaderToAll,
|
|
1541
|
+
onCopyFooterToAll,
|
|
1542
|
+
onClearHeader,
|
|
1543
|
+
onClearFooter,
|
|
1544
|
+
onClearAllHeaders,
|
|
1545
|
+
onClearAllFooters
|
|
1546
|
+
}) => {
|
|
1547
|
+
const hasActivePage = activePageId !== null;
|
|
1548
|
+
const handlePageCounterModeChange = (event) => {
|
|
1549
|
+
const nextMode = event.target.value;
|
|
1550
|
+
if (nextMode === "none" || nextMode === "header" || nextMode === "footer" || nextMode === "both") {
|
|
1551
|
+
onPageCounterModeChange(nextMode);
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1555
|
+
"div",
|
|
1556
|
+
{
|
|
1557
|
+
className: "flex flex-wrap items-center gap-2 text-xs",
|
|
1558
|
+
"data-testid": "header-footer-actions",
|
|
1559
|
+
children: [
|
|
1560
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-gray-500", htmlFor: "page-counter-mode", children: [
|
|
1561
|
+
/* @__PURE__ */ jsxRuntime.jsx(Hash, { size: 13, className: "text-gray-400" }),
|
|
1562
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1563
|
+
"select",
|
|
1564
|
+
{
|
|
1565
|
+
id: "page-counter-mode",
|
|
1566
|
+
className: "h-6 rounded border border-gray-200 bg-white px-1.5 text-xs text-gray-600\n focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400",
|
|
1567
|
+
"data-testid": "page-counter-mode",
|
|
1568
|
+
value: pageCounterMode,
|
|
1569
|
+
onChange: handlePageCounterModeChange,
|
|
1570
|
+
children: [
|
|
1571
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "none", children: "None" }),
|
|
1572
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "header", children: "Header" }),
|
|
1573
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "footer", children: "Footer" }),
|
|
1574
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "both", children: "Both" })
|
|
1575
|
+
]
|
|
1576
|
+
}
|
|
1577
|
+
)
|
|
1578
|
+
] }),
|
|
1579
|
+
/* @__PURE__ */ jsxRuntime.jsx(ActionDivider, {}),
|
|
1580
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 font-medium", children: "Header" }),
|
|
1581
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1582
|
+
ActionIconButton,
|
|
1583
|
+
{
|
|
1584
|
+
title: "Copy header to all pages",
|
|
1585
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(CopyPlus, { size: 13 }),
|
|
1586
|
+
onClick: onCopyHeaderToAll,
|
|
1587
|
+
disabled: !hasActivePage,
|
|
1588
|
+
testId: "copy-header-all"
|
|
1589
|
+
}
|
|
1590
|
+
),
|
|
1591
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1592
|
+
ActionIconButton,
|
|
1593
|
+
{
|
|
1594
|
+
title: "Clear header on this page",
|
|
1595
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(Eraser, { size: 13 }),
|
|
1596
|
+
onClick: onClearHeader,
|
|
1597
|
+
disabled: !hasActivePage,
|
|
1598
|
+
testId: "clear-header"
|
|
1599
|
+
}
|
|
1600
|
+
),
|
|
1601
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1602
|
+
ActionIconButton,
|
|
1603
|
+
{
|
|
1604
|
+
title: "Clear all headers",
|
|
1605
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { size: 13 }),
|
|
1606
|
+
onClick: onClearAllHeaders,
|
|
1607
|
+
testId: "clear-all-headers"
|
|
1608
|
+
}
|
|
1609
|
+
),
|
|
1610
|
+
/* @__PURE__ */ jsxRuntime.jsx(ActionDivider, {}),
|
|
1611
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 font-medium", children: "Footer" }),
|
|
1612
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1613
|
+
ActionIconButton,
|
|
1614
|
+
{
|
|
1615
|
+
title: "Copy footer to all pages",
|
|
1616
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(CopyPlus, { size: 13 }),
|
|
1617
|
+
onClick: onCopyFooterToAll,
|
|
1618
|
+
disabled: !hasActivePage,
|
|
1619
|
+
testId: "copy-footer-all"
|
|
1620
|
+
}
|
|
1621
|
+
),
|
|
1622
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1623
|
+
ActionIconButton,
|
|
1624
|
+
{
|
|
1625
|
+
title: "Clear footer on this page",
|
|
1626
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(Eraser, { size: 13 }),
|
|
1627
|
+
onClick: onClearFooter,
|
|
1628
|
+
disabled: !hasActivePage,
|
|
1629
|
+
testId: "clear-footer"
|
|
1630
|
+
}
|
|
1631
|
+
),
|
|
1632
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1633
|
+
ActionIconButton,
|
|
1634
|
+
{
|
|
1635
|
+
title: "Clear all footers",
|
|
1636
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { size: 13 }),
|
|
1637
|
+
onClick: onClearAllFooters,
|
|
1638
|
+
testId: "clear-all-footers"
|
|
1639
|
+
}
|
|
1640
|
+
)
|
|
1641
|
+
]
|
|
1642
|
+
}
|
|
1643
|
+
);
|
|
1644
|
+
};
|
|
1645
|
+
const ActionIconButton = ({ title, icon, onClick, disabled, testId }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1646
|
+
"button",
|
|
1647
|
+
{
|
|
1648
|
+
type: "button",
|
|
1649
|
+
title,
|
|
1650
|
+
"aria-label": title,
|
|
1651
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1652
|
+
onClick,
|
|
1653
|
+
disabled,
|
|
1654
|
+
className: "flex h-6 w-6 items-center justify-center rounded text-gray-500 transition-colors\n hover:bg-gray-200/60 hover:text-gray-700\n disabled:cursor-not-allowed disabled:text-gray-300",
|
|
1655
|
+
"data-testid": testId,
|
|
1656
|
+
children: icon
|
|
1657
|
+
}
|
|
1658
|
+
);
|
|
1659
|
+
const ActionDivider = () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-0.5 h-4 w-px bg-gray-300/60" });
|
|
1660
|
+
const SUPPORTED_FONTS = [
|
|
1661
|
+
"Times New Roman",
|
|
1662
|
+
"Arial",
|
|
1663
|
+
"Calibri",
|
|
1664
|
+
"Georgia",
|
|
1665
|
+
"Courier New"
|
|
1666
|
+
];
|
|
1667
|
+
function applyFontFamily(editor, fontFamily) {
|
|
1668
|
+
editor.update(() => {
|
|
1669
|
+
const selection = lexical.$getSelection();
|
|
1670
|
+
if (!lexical.$isRangeSelection(selection)) return;
|
|
1671
|
+
const nodes = selection.getNodes();
|
|
1672
|
+
for (const node of nodes) {
|
|
1673
|
+
if (lexical.$isTextNode(node)) {
|
|
1674
|
+
node.setStyle(`font-family: ${fontFamily}`);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
function toggleFormat(editor, format) {
|
|
1680
|
+
editor.dispatchCommand(lexical.FORMAT_TEXT_COMMAND, format);
|
|
1681
|
+
}
|
|
1682
|
+
function toggleBold(editor) {
|
|
1683
|
+
toggleFormat(editor, "bold");
|
|
1684
|
+
}
|
|
1685
|
+
function toggleItalic(editor) {
|
|
1686
|
+
toggleFormat(editor, "italic");
|
|
1687
|
+
}
|
|
1688
|
+
function toggleUnderline(editor) {
|
|
1689
|
+
toggleFormat(editor, "underline");
|
|
1690
|
+
}
|
|
1691
|
+
function toggleStrikethrough(editor) {
|
|
1692
|
+
toggleFormat(editor, "strikethrough");
|
|
1693
|
+
}
|
|
1694
|
+
function setAlignment(editor, alignment) {
|
|
1695
|
+
editor.dispatchCommand(lexical.FORMAT_ELEMENT_COMMAND, alignment);
|
|
1696
|
+
}
|
|
1697
|
+
function insertList(editor, type) {
|
|
1698
|
+
if (type === "number") {
|
|
1699
|
+
editor.dispatchCommand(list.INSERT_ORDERED_LIST_COMMAND, void 0);
|
|
1700
|
+
} else {
|
|
1701
|
+
editor.dispatchCommand(list.INSERT_UNORDERED_LIST_COMMAND, void 0);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function indentContent(editor) {
|
|
1705
|
+
editor.dispatchCommand(lexical.INDENT_CONTENT_COMMAND, void 0);
|
|
1706
|
+
}
|
|
1707
|
+
function outdentContent(editor) {
|
|
1708
|
+
editor.dispatchCommand(lexical.OUTDENT_CONTENT_COMMAND, void 0);
|
|
1709
|
+
}
|
|
1710
|
+
const Toolbar = () => {
|
|
1711
|
+
const {
|
|
1712
|
+
document: document2,
|
|
1713
|
+
dispatch,
|
|
1714
|
+
activePageId,
|
|
1715
|
+
activeEditor,
|
|
1716
|
+
canRedo,
|
|
1717
|
+
canUndo,
|
|
1718
|
+
editorRegistry,
|
|
1719
|
+
globalSelectionActive,
|
|
1720
|
+
historySidebarOpen,
|
|
1721
|
+
redo,
|
|
1722
|
+
runHistoryAction,
|
|
1723
|
+
setHistorySidebarOpen,
|
|
1724
|
+
undo
|
|
1725
|
+
} = useDocument();
|
|
1726
|
+
const withBodySelection = React.useCallback(
|
|
1727
|
+
(editor, action) => {
|
|
1728
|
+
editor.update(() => {
|
|
1729
|
+
lexical.$selectAll();
|
|
1730
|
+
});
|
|
1731
|
+
action(editor);
|
|
1732
|
+
},
|
|
1733
|
+
[]
|
|
1734
|
+
);
|
|
1735
|
+
const applyToBodyEditors = React.useCallback(
|
|
1736
|
+
(action) => {
|
|
1737
|
+
const targetEditors = globalSelectionActive ? editorRegistry.all() : activeEditor ? [activeEditor] : [];
|
|
1738
|
+
targetEditors.forEach((editor) => {
|
|
1739
|
+
if (globalSelectionActive) {
|
|
1740
|
+
withBodySelection(editor, action);
|
|
1741
|
+
} else {
|
|
1742
|
+
action(editor);
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
},
|
|
1746
|
+
[activeEditor, editorRegistry, globalSelectionActive, withBodySelection]
|
|
1747
|
+
);
|
|
1748
|
+
const runToolbarAction = React.useCallback(
|
|
1749
|
+
(label, callback) => {
|
|
1750
|
+
runHistoryAction(
|
|
1751
|
+
{
|
|
1752
|
+
label,
|
|
1753
|
+
source: "toolbar",
|
|
1754
|
+
region: "document"
|
|
1755
|
+
},
|
|
1756
|
+
callback
|
|
1757
|
+
);
|
|
1758
|
+
},
|
|
1759
|
+
[runHistoryAction]
|
|
1760
|
+
);
|
|
1761
|
+
const handleToggle = (enabled) => {
|
|
1762
|
+
runToolbarAction(
|
|
1763
|
+
enabled ? "Enabled headers and footers" : "Disabled headers and footers",
|
|
1764
|
+
() => {
|
|
1765
|
+
dispatch({ type: "SET_HEADER_FOOTER_ENABLED", enabled });
|
|
1766
|
+
}
|
|
1767
|
+
);
|
|
1768
|
+
};
|
|
1769
|
+
const handleCopyHeaderToAll = () => {
|
|
1770
|
+
if (activePageId) {
|
|
1771
|
+
runToolbarAction("Copied header to all pages", () => {
|
|
1772
|
+
dispatch({ type: "COPY_HEADER_TO_ALL", sourcePageId: activePageId });
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
const handleCopyFooterToAll = () => {
|
|
1777
|
+
if (activePageId) {
|
|
1778
|
+
runToolbarAction("Copied footer to all pages", () => {
|
|
1779
|
+
dispatch({ type: "COPY_FOOTER_TO_ALL", sourcePageId: activePageId });
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
const handleClearHeader = () => {
|
|
1784
|
+
if (activePageId) {
|
|
1785
|
+
runToolbarAction("Cleared header", () => {
|
|
1786
|
+
dispatch({ type: "CLEAR_HEADER", pageId: activePageId });
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
const handleClearFooter = () => {
|
|
1791
|
+
if (activePageId) {
|
|
1792
|
+
runToolbarAction("Cleared footer", () => {
|
|
1793
|
+
dispatch({ type: "CLEAR_FOOTER", pageId: activePageId });
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
const handleClearAllHeaders = () => runToolbarAction("Cleared all headers", () => {
|
|
1798
|
+
dispatch({ type: "CLEAR_ALL_HEADERS" });
|
|
1799
|
+
});
|
|
1800
|
+
const handleClearAllFooters = () => runToolbarAction("Cleared all footers", () => {
|
|
1801
|
+
dispatch({ type: "CLEAR_ALL_FOOTERS" });
|
|
1802
|
+
});
|
|
1803
|
+
const handlePageCounterModeChange = React.useCallback((mode) => {
|
|
1804
|
+
runToolbarAction(`Page counter set to ${mode}`, () => {
|
|
1805
|
+
dispatch({ type: "SET_PAGE_COUNTER_MODE", mode });
|
|
1806
|
+
});
|
|
1807
|
+
}, [dispatch, runToolbarAction]);
|
|
1808
|
+
const handleBold = React.useCallback(() => {
|
|
1809
|
+
debug("toolbar", `bold (globalSelection=${globalSelectionActive}, editors=${editorRegistry.all().length}, hasEditor=${!!activeEditor})`);
|
|
1810
|
+
runToolbarAction("Bold applied", () => {
|
|
1811
|
+
applyToBodyEditors(toggleBold);
|
|
1812
|
+
});
|
|
1813
|
+
}, [activeEditor, applyToBodyEditors, editorRegistry, globalSelectionActive, runToolbarAction]);
|
|
1814
|
+
const handleItalic = React.useCallback(() => {
|
|
1815
|
+
debug("toolbar", `italic (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
|
|
1816
|
+
runToolbarAction("Italic applied", () => {
|
|
1817
|
+
applyToBodyEditors(toggleItalic);
|
|
1818
|
+
});
|
|
1819
|
+
}, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction]);
|
|
1820
|
+
const handleUnderline = React.useCallback(() => {
|
|
1821
|
+
debug("toolbar", `underline (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
|
|
1822
|
+
runToolbarAction("Underline applied", () => {
|
|
1823
|
+
applyToBodyEditors(toggleUnderline);
|
|
1824
|
+
});
|
|
1825
|
+
}, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction]);
|
|
1826
|
+
const handleStrikethrough = React.useCallback(() => {
|
|
1827
|
+
debug("toolbar", `strikethrough (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
|
|
1828
|
+
runToolbarAction("Strikethrough applied", () => {
|
|
1829
|
+
applyToBodyEditors(toggleStrikethrough);
|
|
1830
|
+
});
|
|
1831
|
+
}, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction]);
|
|
1832
|
+
const handleAlignLeft = React.useCallback(() => {
|
|
1833
|
+
runToolbarAction("Aligned left", () => {
|
|
1834
|
+
applyToBodyEditors((editor) => setAlignment(editor, "left"));
|
|
1835
|
+
});
|
|
1836
|
+
}, [applyToBodyEditors, runToolbarAction]);
|
|
1837
|
+
const handleAlignCenter = React.useCallback(() => {
|
|
1838
|
+
runToolbarAction("Aligned center", () => {
|
|
1839
|
+
applyToBodyEditors((editor) => setAlignment(editor, "center"));
|
|
1840
|
+
});
|
|
1841
|
+
}, [applyToBodyEditors, runToolbarAction]);
|
|
1842
|
+
const handleAlignRight = React.useCallback(() => {
|
|
1843
|
+
runToolbarAction("Aligned right", () => {
|
|
1844
|
+
applyToBodyEditors((editor) => setAlignment(editor, "right"));
|
|
1845
|
+
});
|
|
1846
|
+
}, [applyToBodyEditors, runToolbarAction]);
|
|
1847
|
+
const handleAlignJustify = React.useCallback(() => {
|
|
1848
|
+
runToolbarAction("Justified text", () => {
|
|
1849
|
+
applyToBodyEditors((editor) => setAlignment(editor, "justify"));
|
|
1850
|
+
});
|
|
1851
|
+
}, [applyToBodyEditors, runToolbarAction]);
|
|
1852
|
+
const handleListNumber = React.useCallback(() => {
|
|
1853
|
+
runToolbarAction("Inserted numbered list", () => {
|
|
1854
|
+
applyToBodyEditors((editor) => insertList(editor, "number"));
|
|
1855
|
+
});
|
|
1856
|
+
}, [applyToBodyEditors, runToolbarAction]);
|
|
1857
|
+
const handleListBullet = React.useCallback(() => {
|
|
1858
|
+
runToolbarAction("Inserted bullet list", () => {
|
|
1859
|
+
applyToBodyEditors((editor) => insertList(editor, "bullet"));
|
|
1860
|
+
});
|
|
1861
|
+
}, [applyToBodyEditors, runToolbarAction]);
|
|
1862
|
+
const handleIndent = React.useCallback(() => {
|
|
1863
|
+
runToolbarAction("Indented content", () => {
|
|
1864
|
+
applyToBodyEditors(indentContent);
|
|
1865
|
+
});
|
|
1866
|
+
}, [applyToBodyEditors, runToolbarAction]);
|
|
1867
|
+
const handleOutdent = React.useCallback(() => {
|
|
1868
|
+
runToolbarAction("Outdented content", () => {
|
|
1869
|
+
applyToBodyEditors(outdentContent);
|
|
1870
|
+
});
|
|
1871
|
+
}, [applyToBodyEditors, runToolbarAction]);
|
|
1872
|
+
const handleFontChange = React.useCallback(
|
|
1873
|
+
(e) => {
|
|
1874
|
+
runToolbarAction(`Font changed to ${e.target.value}`, () => {
|
|
1875
|
+
applyToBodyEditors((editor) => applyFontFamily(editor, e.target.value));
|
|
1876
|
+
});
|
|
1877
|
+
},
|
|
1878
|
+
[applyToBodyEditors, runToolbarAction]
|
|
1879
|
+
);
|
|
1880
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1881
|
+
"div",
|
|
1882
|
+
{
|
|
1883
|
+
className: "lex4-toolbar sticky top-0 z-10 bg-white border-b border-gray-200",
|
|
1884
|
+
"data-testid": "toolbar",
|
|
1885
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 px-2 py-1.5", children: [
|
|
1886
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "history-controls", children: [
|
|
1887
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1888
|
+
ToolbarIconButton,
|
|
1889
|
+
{
|
|
1890
|
+
title: "Undo",
|
|
1891
|
+
testId: "btn-undo",
|
|
1892
|
+
disabled: !canUndo,
|
|
1893
|
+
onClick: undo,
|
|
1894
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Undo2, { size: 15 })
|
|
1895
|
+
}
|
|
1896
|
+
),
|
|
1897
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1898
|
+
ToolbarIconButton,
|
|
1899
|
+
{
|
|
1900
|
+
title: "Redo",
|
|
1901
|
+
testId: "btn-redo",
|
|
1902
|
+
disabled: !canRedo,
|
|
1903
|
+
onClick: redo,
|
|
1904
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Redo2, { size: 15 })
|
|
1905
|
+
}
|
|
1906
|
+
)
|
|
1907
|
+
] }),
|
|
1908
|
+
/* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
|
|
1909
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1910
|
+
"select",
|
|
1911
|
+
{
|
|
1912
|
+
className: "h-7 rounded border border-gray-200 bg-white px-2 text-xs text-gray-700\n focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400",
|
|
1913
|
+
"data-testid": "font-selector",
|
|
1914
|
+
defaultValue: "Arial",
|
|
1915
|
+
onChange: handleFontChange,
|
|
1916
|
+
children: SUPPORTED_FONTS.map((font) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: font, style: { fontFamily: font }, children: font }, font))
|
|
1917
|
+
}
|
|
1918
|
+
),
|
|
1919
|
+
/* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
|
|
1920
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "format-group", children: [
|
|
1921
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Bold (Ctrl+B)", testId: "btn-bold", onClick: handleBold, children: /* @__PURE__ */ jsxRuntime.jsx(Bold, { size: 15 }) }),
|
|
1922
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Italic (Ctrl+I)", testId: "btn-italic", onClick: handleItalic, children: /* @__PURE__ */ jsxRuntime.jsx(Italic, { size: 15 }) }),
|
|
1923
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Underline (Ctrl+U)", testId: "btn-underline", onClick: handleUnderline, children: /* @__PURE__ */ jsxRuntime.jsx(Underline, { size: 15 }) }),
|
|
1924
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Strikethrough", testId: "btn-strike", onClick: handleStrikethrough, children: /* @__PURE__ */ jsxRuntime.jsx(Strikethrough, { size: 15 }) })
|
|
1925
|
+
] }),
|
|
1926
|
+
/* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
|
|
1927
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "align-group", children: [
|
|
1928
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Align Left", testId: "btn-align-left", onClick: handleAlignLeft, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignStart, { size: 15 }) }),
|
|
1929
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Align Center", testId: "btn-align-center", onClick: handleAlignCenter, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignCenter, { size: 15 }) }),
|
|
1930
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Align Right", testId: "btn-align-right", onClick: handleAlignRight, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignEnd, { size: 15 }) }),
|
|
1931
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Justify", testId: "btn-align-justify", onClick: handleAlignJustify, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignJustify, { size: 15 }) })
|
|
1932
|
+
] }),
|
|
1933
|
+
/* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
|
|
1934
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "list-group", children: [
|
|
1935
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Numbered List", testId: "btn-list-number", onClick: handleListNumber, children: /* @__PURE__ */ jsxRuntime.jsx(ListOrdered, { size: 15 }) }),
|
|
1936
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Bullet List", testId: "btn-list-bullet", onClick: handleListBullet, children: /* @__PURE__ */ jsxRuntime.jsx(List, { size: 15 }) }),
|
|
1937
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Indent", testId: "btn-indent", onClick: handleIndent, children: /* @__PURE__ */ jsxRuntime.jsx(ListIndentIncrease, { size: 15 }) }),
|
|
1938
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Outdent", testId: "btn-outdent", onClick: handleOutdent, children: /* @__PURE__ */ jsxRuntime.jsx(ListIndentDecrease, { size: 15 }) })
|
|
1939
|
+
] }),
|
|
1940
|
+
/* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
|
|
1941
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1942
|
+
HeaderFooterToggle,
|
|
1943
|
+
{
|
|
1944
|
+
enabled: document2.headerFooterEnabled,
|
|
1945
|
+
onToggle: handleToggle
|
|
1946
|
+
}
|
|
1947
|
+
),
|
|
1948
|
+
document2.headerFooterEnabled && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1949
|
+
/* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
|
|
1950
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1951
|
+
HeaderFooterActions,
|
|
1952
|
+
{
|
|
1953
|
+
activePageId,
|
|
1954
|
+
pageCounterMode: document2.pageCounterMode,
|
|
1955
|
+
onPageCounterModeChange: handlePageCounterModeChange,
|
|
1956
|
+
onCopyHeaderToAll: handleCopyHeaderToAll,
|
|
1957
|
+
onCopyFooterToAll: handleCopyFooterToAll,
|
|
1958
|
+
onClearHeader: handleClearHeader,
|
|
1959
|
+
onClearFooter: handleClearFooter,
|
|
1960
|
+
onClearAllHeaders: handleClearAllHeaders,
|
|
1961
|
+
onClearAllFooters: handleClearAllFooters
|
|
1962
|
+
}
|
|
1963
|
+
)
|
|
1964
|
+
] }),
|
|
1965
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-auto flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1966
|
+
ToolbarIconButton,
|
|
1967
|
+
{
|
|
1968
|
+
title: historySidebarOpen ? "Close History" : "Open History",
|
|
1969
|
+
testId: "toggle-history-sidebar",
|
|
1970
|
+
active: historySidebarOpen,
|
|
1971
|
+
onClick: () => setHistorySidebarOpen(!historySidebarOpen),
|
|
1972
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(PanelRight, { size: 15 })
|
|
1973
|
+
}
|
|
1974
|
+
) })
|
|
1975
|
+
] })
|
|
1976
|
+
}
|
|
1977
|
+
);
|
|
1978
|
+
};
|
|
1979
|
+
const ToolbarIconButton = ({
|
|
1980
|
+
title,
|
|
1981
|
+
testId,
|
|
1982
|
+
disabled = false,
|
|
1983
|
+
active = false,
|
|
1984
|
+
onClick,
|
|
1985
|
+
children
|
|
1986
|
+
}) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1987
|
+
"button",
|
|
1988
|
+
{
|
|
1989
|
+
type: "button",
|
|
1990
|
+
title,
|
|
1991
|
+
"aria-label": title,
|
|
1992
|
+
disabled,
|
|
1993
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1994
|
+
onClick,
|
|
1995
|
+
className: `
|
|
1996
|
+
flex h-7 w-7 items-center justify-center rounded transition-colors
|
|
1997
|
+
${disabled ? "cursor-not-allowed text-gray-300" : active ? "bg-blue-50 text-blue-600" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}
|
|
1998
|
+
`,
|
|
1999
|
+
"data-testid": testId,
|
|
2000
|
+
children
|
|
2001
|
+
}
|
|
2002
|
+
);
|
|
2003
|
+
const Divider = () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-0.5 h-5 w-px bg-gray-200" });
|
|
2004
|
+
function computeBodyHeight(headerHeight, footerHeight) {
|
|
2005
|
+
const verticalMargins = PAGE_MARGIN_PX * 2;
|
|
2006
|
+
return A4_HEIGHT_PX - headerHeight - footerHeight - verticalMargins;
|
|
2007
|
+
}
|
|
2008
|
+
function getTopLevelNodes(state) {
|
|
2009
|
+
if (!state) return [];
|
|
2010
|
+
return state.root.children ?? [];
|
|
2011
|
+
}
|
|
2012
|
+
function createEditorStateFromNodes(nodes) {
|
|
2013
|
+
return {
|
|
2014
|
+
root: {
|
|
2015
|
+
children: nodes,
|
|
2016
|
+
direction: null,
|
|
2017
|
+
format: "",
|
|
2018
|
+
indent: 0,
|
|
2019
|
+
type: "root",
|
|
2020
|
+
version: 1
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
function splitEditorState(state, splitIndex) {
|
|
2025
|
+
const nodes = getTopLevelNodes(state);
|
|
2026
|
+
if (splitIndex <= 0) {
|
|
2027
|
+
return [null, state];
|
|
2028
|
+
}
|
|
2029
|
+
if (splitIndex >= nodes.length) {
|
|
2030
|
+
return [state, null];
|
|
2031
|
+
}
|
|
2032
|
+
const before = nodes.slice(0, splitIndex);
|
|
2033
|
+
const after = nodes.slice(splitIndex);
|
|
2034
|
+
return [
|
|
2035
|
+
before.length > 0 ? createEditorStateFromNodes(before) : null,
|
|
2036
|
+
after.length > 0 ? createEditorStateFromNodes(after) : null
|
|
2037
|
+
];
|
|
2038
|
+
}
|
|
2039
|
+
function mergeEditorStates(stateA, stateB) {
|
|
2040
|
+
const nodesA = getTopLevelNodes(stateA);
|
|
2041
|
+
const nodesB = getTopLevelNodes(stateB);
|
|
2042
|
+
const all = [...nodesA, ...nodesB];
|
|
2043
|
+
if (all.length === 0) return null;
|
|
2044
|
+
return createEditorStateFromNodes(all);
|
|
2045
|
+
}
|
|
2046
|
+
function usePagination(document2, dispatch) {
|
|
2047
|
+
const reflowingRef = React.useRef(false);
|
|
2048
|
+
const pendingReflowRef = React.useRef(false);
|
|
2049
|
+
const estimateNodesFitting = React.useCallback(
|
|
2050
|
+
(state, bodyHeight) => {
|
|
2051
|
+
const nodes = getTopLevelNodes(state);
|
|
2052
|
+
if (nodes.length === 0) return 0;
|
|
2053
|
+
const estimatedLineHeight = 24;
|
|
2054
|
+
let usedHeight = 0;
|
|
2055
|
+
let fitCount = 0;
|
|
2056
|
+
for (const _node of nodes) {
|
|
2057
|
+
usedHeight += estimatedLineHeight;
|
|
2058
|
+
if (usedHeight > bodyHeight) break;
|
|
2059
|
+
fitCount++;
|
|
2060
|
+
}
|
|
2061
|
+
return Math.max(1, fitCount);
|
|
2062
|
+
},
|
|
2063
|
+
[]
|
|
2064
|
+
);
|
|
2065
|
+
const handlePageOverflow = React.useCallback(
|
|
2066
|
+
(pageIndex) => {
|
|
2067
|
+
if (reflowingRef.current) {
|
|
2068
|
+
pendingReflowRef.current = true;
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
reflowingRef.current = true;
|
|
2072
|
+
try {
|
|
2073
|
+
const pages = document2.pages;
|
|
2074
|
+
const page = pages[pageIndex];
|
|
2075
|
+
if (!page) return;
|
|
2076
|
+
const headerH = document2.headerFooterEnabled ? page.headerHeight : 0;
|
|
2077
|
+
const footerH = document2.headerFooterEnabled ? page.footerHeight : 0;
|
|
2078
|
+
const bodyHeight = computeBodyHeight(headerH, footerH);
|
|
2079
|
+
const nodes = getTopLevelNodes(page.bodyState);
|
|
2080
|
+
if (nodes.length <= 1) return;
|
|
2081
|
+
const fitCount = estimateNodesFitting(page.bodyState, bodyHeight);
|
|
2082
|
+
if (fitCount >= nodes.length) return;
|
|
2083
|
+
const [keepState, overflowState] = splitEditorState(page.bodyState, fitCount);
|
|
2084
|
+
if (!overflowState) return;
|
|
2085
|
+
const nextPage = pages[pageIndex + 1];
|
|
2086
|
+
if (nextPage) {
|
|
2087
|
+
const mergedBody = mergeEditorStates(overflowState, nextPage.bodyState);
|
|
2088
|
+
dispatch({
|
|
2089
|
+
type: "SET_DOCUMENT",
|
|
2090
|
+
document: {
|
|
2091
|
+
...document2,
|
|
2092
|
+
pages: pages.map((p, i) => {
|
|
2093
|
+
if (i === pageIndex) return { ...p, bodyState: keepState };
|
|
2094
|
+
if (i === pageIndex + 1) return { ...p, bodyState: mergedBody };
|
|
2095
|
+
return p;
|
|
2096
|
+
})
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
} else {
|
|
2100
|
+
const newPage = {
|
|
2101
|
+
...createPageFromTemplate({
|
|
2102
|
+
headerState: document2.defaultHeaderState,
|
|
2103
|
+
footerState: document2.defaultFooterState,
|
|
2104
|
+
headerHeight: document2.defaultHeaderHeight,
|
|
2105
|
+
footerHeight: document2.defaultFooterHeight
|
|
2106
|
+
}),
|
|
2107
|
+
bodyState: overflowState
|
|
2108
|
+
};
|
|
2109
|
+
dispatch({
|
|
2110
|
+
type: "SET_DOCUMENT",
|
|
2111
|
+
document: {
|
|
2112
|
+
...document2,
|
|
2113
|
+
pages: [
|
|
2114
|
+
...pages.map(
|
|
2115
|
+
(p, i) => i === pageIndex ? { ...p, bodyState: keepState } : p
|
|
2116
|
+
),
|
|
2117
|
+
newPage
|
|
2118
|
+
]
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
} finally {
|
|
2123
|
+
reflowingRef.current = false;
|
|
2124
|
+
if (pendingReflowRef.current) {
|
|
2125
|
+
pendingReflowRef.current = false;
|
|
2126
|
+
setTimeout(() => handlePageOverflow(pageIndex), 0);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
},
|
|
2130
|
+
[document2, dispatch, estimateNodesFitting]
|
|
2131
|
+
);
|
|
2132
|
+
const handlePageUnderflow = React.useCallback(
|
|
2133
|
+
(pageIndex) => {
|
|
2134
|
+
if (reflowingRef.current) return;
|
|
2135
|
+
const pages = document2.pages;
|
|
2136
|
+
const page = pages[pageIndex];
|
|
2137
|
+
const nextPage = pages[pageIndex + 1];
|
|
2138
|
+
if (!page || !nextPage) return;
|
|
2139
|
+
const nextNodes = getTopLevelNodes(nextPage.bodyState);
|
|
2140
|
+
if (nextNodes.length === 0) {
|
|
2141
|
+
if (pages.length > 1) {
|
|
2142
|
+
dispatch({
|
|
2143
|
+
type: "SET_DOCUMENT",
|
|
2144
|
+
document: {
|
|
2145
|
+
...document2,
|
|
2146
|
+
pages: pages.filter((_, i) => i !== pageIndex + 1)
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
const headerH = document2.headerFooterEnabled ? page.headerHeight : 0;
|
|
2153
|
+
const footerH = document2.headerFooterEnabled ? page.footerHeight : 0;
|
|
2154
|
+
const bodyHeight = computeBodyHeight(headerH, footerH);
|
|
2155
|
+
const currentNodes = getTopLevelNodes(page.bodyState);
|
|
2156
|
+
const estimatedCurrentHeight = currentNodes.length * 24;
|
|
2157
|
+
const remainingSpace = bodyHeight - estimatedCurrentHeight;
|
|
2158
|
+
if (remainingSpace > 24) {
|
|
2159
|
+
const [pulled, remaining] = [nextNodes.slice(0, 1), nextNodes.slice(1)];
|
|
2160
|
+
const updatedCurrent = mergeEditorStates(
|
|
2161
|
+
page.bodyState,
|
|
2162
|
+
createEditorStateFromNodes(pulled)
|
|
2163
|
+
);
|
|
2164
|
+
const updatedNext = remaining.length > 0 ? createEditorStateFromNodes(remaining) : null;
|
|
2165
|
+
const newPages = pages.map((p, i) => {
|
|
2166
|
+
if (i === pageIndex) return { ...p, bodyState: updatedCurrent };
|
|
2167
|
+
if (i === pageIndex + 1) return { ...p, bodyState: updatedNext };
|
|
2168
|
+
return p;
|
|
2169
|
+
});
|
|
2170
|
+
while (newPages.length > 1) {
|
|
2171
|
+
const last = newPages[newPages.length - 1];
|
|
2172
|
+
if (getTopLevelNodes(last.bodyState).length === 0) {
|
|
2173
|
+
newPages.pop();
|
|
2174
|
+
} else {
|
|
2175
|
+
break;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
dispatch({
|
|
2179
|
+
type: "SET_DOCUMENT",
|
|
2180
|
+
document: { ...document2, pages: newPages }
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
},
|
|
2184
|
+
[document2, dispatch]
|
|
2185
|
+
);
|
|
2186
|
+
const reflowAll = React.useCallback(() => {
|
|
2187
|
+
if (reflowingRef.current) return;
|
|
2188
|
+
reflowingRef.current = true;
|
|
2189
|
+
try {
|
|
2190
|
+
const pages = document2.pages;
|
|
2191
|
+
const allNodes = pages.flatMap((p) => getTopLevelNodes(p.bodyState));
|
|
2192
|
+
if (allNodes.length === 0) {
|
|
2193
|
+
if (pages.length !== 1) {
|
|
2194
|
+
dispatch({
|
|
2195
|
+
type: "SET_DOCUMENT",
|
|
2196
|
+
document: {
|
|
2197
|
+
...document2,
|
|
2198
|
+
pages: [pages[0] ?? createEmptyPage()]
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const newPages = [];
|
|
2205
|
+
let remainingNodes = [...allNodes];
|
|
2206
|
+
let pageIdx = 0;
|
|
2207
|
+
while (remainingNodes.length > 0) {
|
|
2208
|
+
const existingPage = pages[pageIdx];
|
|
2209
|
+
const headerH = document2.headerFooterEnabled ? (existingPage == null ? void 0 : existingPage.headerHeight) ?? 0 : 0;
|
|
2210
|
+
const footerH = document2.headerFooterEnabled ? (existingPage == null ? void 0 : existingPage.footerHeight) ?? 0 : 0;
|
|
2211
|
+
const bodyHeight = computeBodyHeight(headerH, footerH);
|
|
2212
|
+
const estimatedLineHeight = 24;
|
|
2213
|
+
const maxNodes = Math.max(1, Math.floor(bodyHeight / estimatedLineHeight));
|
|
2214
|
+
const pageNodes = remainingNodes.slice(0, maxNodes);
|
|
2215
|
+
remainingNodes = remainingNodes.slice(maxNodes);
|
|
2216
|
+
const basePage = existingPage ?? createPageFromTemplate({
|
|
2217
|
+
headerState: document2.defaultHeaderState,
|
|
2218
|
+
footerState: document2.defaultFooterState,
|
|
2219
|
+
headerHeight: document2.defaultHeaderHeight,
|
|
2220
|
+
footerHeight: document2.defaultFooterHeight
|
|
2221
|
+
});
|
|
2222
|
+
newPages.push({
|
|
2223
|
+
...basePage,
|
|
2224
|
+
bodyState: createEditorStateFromNodes(pageNodes)
|
|
2225
|
+
});
|
|
2226
|
+
pageIdx++;
|
|
2227
|
+
}
|
|
2228
|
+
dispatch({
|
|
2229
|
+
type: "SET_DOCUMENT",
|
|
2230
|
+
document: { ...document2, pages: newPages }
|
|
2231
|
+
});
|
|
2232
|
+
} finally {
|
|
2233
|
+
reflowingRef.current = false;
|
|
2234
|
+
}
|
|
2235
|
+
}, [document2, dispatch]);
|
|
2236
|
+
return {
|
|
2237
|
+
handlePageOverflow,
|
|
2238
|
+
handlePageUnderflow,
|
|
2239
|
+
reflowAll
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
const lexicalTheme = {
|
|
2243
|
+
root: "lex4-root outline-none",
|
|
2244
|
+
paragraph: "lex4-paragraph mb-1",
|
|
2245
|
+
heading: {
|
|
2246
|
+
h1: "text-3xl font-bold mb-2",
|
|
2247
|
+
h2: "text-2xl font-bold mb-2",
|
|
2248
|
+
h3: "text-xl font-semibold mb-1",
|
|
2249
|
+
h4: "text-lg font-semibold mb-1",
|
|
2250
|
+
h5: "text-base font-medium mb-1"
|
|
2251
|
+
},
|
|
2252
|
+
text: {
|
|
2253
|
+
bold: "font-bold",
|
|
2254
|
+
italic: "italic",
|
|
2255
|
+
underline: "underline",
|
|
2256
|
+
strikethrough: "line-through",
|
|
2257
|
+
underlineStrikethrough: "underline line-through"
|
|
2258
|
+
},
|
|
2259
|
+
list: {
|
|
2260
|
+
nested: {
|
|
2261
|
+
listitem: "list-none"
|
|
2262
|
+
},
|
|
2263
|
+
ol: "list-decimal ml-6",
|
|
2264
|
+
ul: "list-disc ml-6",
|
|
2265
|
+
listitem: "lex4-listitem",
|
|
2266
|
+
listitemChecked: "lex4-listitem-checked",
|
|
2267
|
+
listitemUnchecked: "lex4-listitem-unchecked"
|
|
2268
|
+
},
|
|
2269
|
+
quote: "border-l-4 border-gray-300 pl-4 italic text-gray-600"
|
|
2270
|
+
};
|
|
2271
|
+
function createEditorConfig(mode, pageId) {
|
|
2272
|
+
const namespace = pageId ? `lex4-${mode}-${pageId}` : `lex4-${mode}`;
|
|
2273
|
+
return {
|
|
2274
|
+
namespace,
|
|
2275
|
+
theme: lexicalTheme,
|
|
2276
|
+
nodes: [richText.HeadingNode, richText.QuoteNode, list.ListNode, list.ListItemNode],
|
|
2277
|
+
onError: (error) => {
|
|
2278
|
+
console.error(`[Lex4 ${mode} editor error]`, error);
|
|
2279
|
+
}
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
const TabIndentPlugin = () => {
|
|
2283
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2284
|
+
const handleTab = React.useCallback(
|
|
2285
|
+
(event) => {
|
|
2286
|
+
event.preventDefault();
|
|
2287
|
+
if (event.shiftKey) {
|
|
2288
|
+
outdentContent(editor);
|
|
2289
|
+
} else {
|
|
2290
|
+
indentContent(editor);
|
|
2291
|
+
}
|
|
2292
|
+
return true;
|
|
2293
|
+
},
|
|
2294
|
+
[editor]
|
|
2295
|
+
);
|
|
2296
|
+
React.useEffect(() => {
|
|
2297
|
+
return editor.registerCommand(lexical.KEY_TAB_COMMAND, handleTab, lexical.COMMAND_PRIORITY_LOW);
|
|
2298
|
+
}, [editor, handleTab]);
|
|
2299
|
+
return null;
|
|
2300
|
+
};
|
|
2301
|
+
const FIRST_LINE_INDENT_PX = 40;
|
|
2302
|
+
function visitElements(node, callback) {
|
|
2303
|
+
callback(node);
|
|
2304
|
+
for (const child of node.getChildren()) {
|
|
2305
|
+
if (lexical.$isElementNode(child)) {
|
|
2306
|
+
visitElements(child, callback);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
const ParagraphIndentPlugin = () => {
|
|
2311
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2312
|
+
React.useEffect(() => {
|
|
2313
|
+
const syncParagraphIndents = () => editor.getEditorState().read(() => {
|
|
2314
|
+
visitElements(lexical.$getRoot(), (node) => {
|
|
2315
|
+
if (!lexical.$isParagraphNode(node)) {
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
const element = editor.getElementByKey(node.getKey());
|
|
2319
|
+
if (!(element instanceof HTMLElement)) {
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
const indent = node.getIndent();
|
|
2323
|
+
element.style.paddingInlineStart = "0px";
|
|
2324
|
+
element.style.textIndent = indent > 0 ? `${indent * FIRST_LINE_INDENT_PX}px` : "";
|
|
2325
|
+
});
|
|
2326
|
+
});
|
|
2327
|
+
syncParagraphIndents();
|
|
2328
|
+
return editor.registerUpdateListener(() => {
|
|
2329
|
+
syncParagraphIndents();
|
|
2330
|
+
});
|
|
2331
|
+
}, [editor]);
|
|
2332
|
+
return null;
|
|
2333
|
+
};
|
|
2334
|
+
const PastePlugin = () => {
|
|
2335
|
+
return null;
|
|
2336
|
+
};
|
|
2337
|
+
function resolveTextOffset(root, targetOffset) {
|
|
2338
|
+
var _a, _b;
|
|
2339
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
2340
|
+
let remaining = targetOffset;
|
|
2341
|
+
let currentNode = walker.nextNode();
|
|
2342
|
+
let lastTextNode = null;
|
|
2343
|
+
while (currentNode) {
|
|
2344
|
+
const textLength = ((_a = currentNode.textContent) == null ? void 0 : _a.length) ?? 0;
|
|
2345
|
+
if (remaining <= textLength) {
|
|
2346
|
+
return { node: currentNode, offset: remaining };
|
|
2347
|
+
}
|
|
2348
|
+
remaining -= textLength;
|
|
2349
|
+
lastTextNode = currentNode;
|
|
2350
|
+
currentNode = walker.nextNode();
|
|
2351
|
+
}
|
|
2352
|
+
if (lastTextNode) {
|
|
2353
|
+
return {
|
|
2354
|
+
node: lastTextNode,
|
|
2355
|
+
offset: ((_b = lastTextNode.textContent) == null ? void 0 : _b.length) ?? 0
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
return {
|
|
2359
|
+
node: root,
|
|
2360
|
+
offset: root.childNodes.length
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
const ActiveEditorPlugin = ({
|
|
2364
|
+
pageId,
|
|
2365
|
+
region,
|
|
2366
|
+
onFocus
|
|
2367
|
+
}) => {
|
|
2368
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2369
|
+
const {
|
|
2370
|
+
consumePendingCaretPosition,
|
|
2371
|
+
consumePendingFocusAtEnd,
|
|
2372
|
+
focusAtEndVersion,
|
|
2373
|
+
setActiveEditor,
|
|
2374
|
+
setActivePageId
|
|
2375
|
+
} = useDocument();
|
|
2376
|
+
React.useEffect(() => {
|
|
2377
|
+
const caretPosition = { pageId, region };
|
|
2378
|
+
const selectionToRestore = consumePendingCaretPosition(caretPosition);
|
|
2379
|
+
if (selectionToRestore === void 0) {
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
requestAnimationFrame(() => {
|
|
2383
|
+
setActivePageId(pageId);
|
|
2384
|
+
setActiveEditor(editor, caretPosition);
|
|
2385
|
+
editor.focus();
|
|
2386
|
+
if (!selectionToRestore) {
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
requestAnimationFrame(() => {
|
|
2390
|
+
const rootElement = editor.getRootElement();
|
|
2391
|
+
const domSelection = window.getSelection();
|
|
2392
|
+
if (!rootElement || !domSelection) {
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const anchorPoint = resolveTextOffset(rootElement, selectionToRestore.anchorTextOffset);
|
|
2396
|
+
const focusPoint = resolveTextOffset(rootElement, selectionToRestore.focusTextOffset);
|
|
2397
|
+
const range = document.createRange();
|
|
2398
|
+
range.setStart(anchorPoint.node, anchorPoint.offset);
|
|
2399
|
+
range.setEnd(focusPoint.node, focusPoint.offset);
|
|
2400
|
+
domSelection.removeAllRanges();
|
|
2401
|
+
domSelection.addRange(range);
|
|
2402
|
+
editor.update(() => {
|
|
2403
|
+
const nextSelection = lexical.$createRangeSelectionFromDom(window.getSelection(), editor);
|
|
2404
|
+
if (nextSelection) {
|
|
2405
|
+
lexical.$setSelection(nextSelection);
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2408
|
+
});
|
|
2409
|
+
});
|
|
2410
|
+
}, [consumePendingCaretPosition, editor, pageId, region, setActiveEditor, setActivePageId]);
|
|
2411
|
+
React.useEffect(() => {
|
|
2412
|
+
const caretPosition = { pageId, region };
|
|
2413
|
+
if (!consumePendingFocusAtEnd(caretPosition)) {
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
requestAnimationFrame(() => {
|
|
2417
|
+
setActivePageId(pageId);
|
|
2418
|
+
setActiveEditor(editor, caretPosition);
|
|
2419
|
+
editor.focus(() => {
|
|
2420
|
+
editor.update(() => {
|
|
2421
|
+
lexical.$getRoot().selectEnd();
|
|
2422
|
+
});
|
|
2423
|
+
});
|
|
2424
|
+
});
|
|
2425
|
+
}, [
|
|
2426
|
+
consumePendingFocusAtEnd,
|
|
2427
|
+
editor,
|
|
2428
|
+
focusAtEndVersion,
|
|
2429
|
+
pageId,
|
|
2430
|
+
region,
|
|
2431
|
+
setActiveEditor,
|
|
2432
|
+
setActivePageId
|
|
2433
|
+
]);
|
|
2434
|
+
React.useEffect(() => {
|
|
2435
|
+
return editor.registerCommand(
|
|
2436
|
+
lexical.FOCUS_COMMAND,
|
|
2437
|
+
() => {
|
|
2438
|
+
debug("focus", `editor focused (ns=${editor.getKey()})`);
|
|
2439
|
+
setActivePageId(pageId);
|
|
2440
|
+
setActiveEditor(editor, { pageId, region });
|
|
2441
|
+
onFocus == null ? void 0 : onFocus(editor);
|
|
2442
|
+
return false;
|
|
2443
|
+
},
|
|
2444
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
2445
|
+
);
|
|
2446
|
+
}, [editor, onFocus, pageId, region, setActiveEditor, setActivePageId]);
|
|
2447
|
+
return null;
|
|
2448
|
+
};
|
|
2449
|
+
function serializeNodeTree(node) {
|
|
2450
|
+
const json = node.exportJSON();
|
|
2451
|
+
if (lexical.$isElementNode(node)) {
|
|
2452
|
+
json.children = node.getChildren().map(serializeNodeTree);
|
|
2453
|
+
}
|
|
2454
|
+
return json;
|
|
2455
|
+
}
|
|
2456
|
+
const OVERFLOW_DEBOUNCE_MS = 300;
|
|
2457
|
+
const OverflowPlugin = ({
|
|
2458
|
+
onOverflow
|
|
2459
|
+
}) => {
|
|
2460
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2461
|
+
const processingRef = React.useRef(false);
|
|
2462
|
+
const onOverflowRef = React.useRef(onOverflow);
|
|
2463
|
+
onOverflowRef.current = onOverflow;
|
|
2464
|
+
React.useEffect(() => {
|
|
2465
|
+
let observer = null;
|
|
2466
|
+
let unregisterUpdateListener = null;
|
|
2467
|
+
let debounceTimer = null;
|
|
2468
|
+
const checkOverflow = (rootElement, container, cause) => {
|
|
2469
|
+
if (processingRef.current) return;
|
|
2470
|
+
const availableHeight = container.clientHeight;
|
|
2471
|
+
if (availableHeight <= 0) return;
|
|
2472
|
+
const contentHeight = rootElement.scrollHeight;
|
|
2473
|
+
if (contentHeight <= availableHeight + 2) return;
|
|
2474
|
+
const children = Array.from(rootElement.children);
|
|
2475
|
+
if (children.length === 0) return;
|
|
2476
|
+
if (children.length <= 1) {
|
|
2477
|
+
debug("overflow", `single block overflows (content=${contentHeight}px > available=${availableHeight}px) — cannot split`);
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
processingRef.current = true;
|
|
2481
|
+
debug("overflow", `OVERFLOW detected: content=${contentHeight}px available=${availableHeight}px children=${children.length}`);
|
|
2482
|
+
let splitIndex = children.length;
|
|
2483
|
+
for (let i = 0; i < children.length; i++) {
|
|
2484
|
+
const child = children[i];
|
|
2485
|
+
const childBottom = child.offsetTop + child.offsetHeight;
|
|
2486
|
+
if (childBottom > availableHeight && i > 0) {
|
|
2487
|
+
splitIndex = i;
|
|
2488
|
+
debug("overflow", `split at index ${i} (childBottom=${childBottom}px > ${availableHeight}px)`);
|
|
2489
|
+
break;
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
if (splitIndex >= children.length) {
|
|
2493
|
+
debugWarn("overflow", "no valid split point found — all children fit individually");
|
|
2494
|
+
processingRef.current = false;
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
editor.update(
|
|
2498
|
+
() => {
|
|
2499
|
+
const root = lexical.$getRoot();
|
|
2500
|
+
const allChildren = root.getChildren();
|
|
2501
|
+
if (splitIndex >= allChildren.length) {
|
|
2502
|
+
debugWarn("overflow", `splitIndex=${splitIndex} >= Lexical children=${allChildren.length} — mismatch`);
|
|
2503
|
+
processingRef.current = false;
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
const overflowNodes = [];
|
|
2507
|
+
const toRemove = allChildren.slice(splitIndex);
|
|
2508
|
+
for (const node of toRemove) {
|
|
2509
|
+
overflowNodes.push(serializeNodeTree(node));
|
|
2510
|
+
}
|
|
2511
|
+
for (const node of toRemove) {
|
|
2512
|
+
node.remove();
|
|
2513
|
+
}
|
|
2514
|
+
debug("overflow", `extracted ${overflowNodes.length} overflow nodes, kept ${splitIndex} nodes on current page`);
|
|
2515
|
+
const overflowState = {
|
|
2516
|
+
root: {
|
|
2517
|
+
children: overflowNodes,
|
|
2518
|
+
direction: null,
|
|
2519
|
+
format: "",
|
|
2520
|
+
indent: 0,
|
|
2521
|
+
type: "root",
|
|
2522
|
+
version: 1
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
setTimeout(() => {
|
|
2526
|
+
onOverflowRef.current(overflowState, cause);
|
|
2527
|
+
processingRef.current = false;
|
|
2528
|
+
}, 0);
|
|
2529
|
+
},
|
|
2530
|
+
{ tag: "overflow-split" }
|
|
2531
|
+
);
|
|
2532
|
+
};
|
|
2533
|
+
const setupObservers = (rootElement) => {
|
|
2534
|
+
if (observer) observer.disconnect();
|
|
2535
|
+
if (unregisterUpdateListener) unregisterUpdateListener();
|
|
2536
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2537
|
+
const container = rootElement.closest(".lex4-page-body");
|
|
2538
|
+
if (!container) {
|
|
2539
|
+
debugWarn("overflow", "no .lex4-page-body container found");
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
debug("overflow", `observers attached (ns=${editor.getKey()})`);
|
|
2543
|
+
const immediateCheck = () => {
|
|
2544
|
+
requestAnimationFrame(() => checkOverflow(rootElement, container, "content"));
|
|
2545
|
+
};
|
|
2546
|
+
const pasteCheck = () => {
|
|
2547
|
+
requestAnimationFrame(() => checkOverflow(rootElement, container, "paste"));
|
|
2548
|
+
};
|
|
2549
|
+
const debouncedCheck = () => {
|
|
2550
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2551
|
+
debounceTimer = setTimeout(immediateCheck, OVERFLOW_DEBOUNCE_MS);
|
|
2552
|
+
};
|
|
2553
|
+
observer = new ResizeObserver(debouncedCheck);
|
|
2554
|
+
observer.observe(rootElement);
|
|
2555
|
+
observer.observe(container);
|
|
2556
|
+
unregisterUpdateListener = editor.registerUpdateListener(({ tags }) => {
|
|
2557
|
+
if (tags.has("overflow-split")) return;
|
|
2558
|
+
if (tags.has("paste")) {
|
|
2559
|
+
debug("overflow", `paste check (tags=${Array.from(tags).join(",")})`);
|
|
2560
|
+
pasteCheck();
|
|
2561
|
+
} else if (tags.has("collaboration") || tags.has("historic")) {
|
|
2562
|
+
debug("overflow", `immediate check (tags=${Array.from(tags).join(",")})`);
|
|
2563
|
+
immediateCheck();
|
|
2564
|
+
} else {
|
|
2565
|
+
debouncedCheck();
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
immediateCheck();
|
|
2569
|
+
};
|
|
2570
|
+
const unregisterRootListener = editor.registerRootListener(
|
|
2571
|
+
(rootElement) => {
|
|
2572
|
+
if (rootElement) {
|
|
2573
|
+
debug("overflow", `rootListener: root available (ns=${editor.getKey()})`);
|
|
2574
|
+
setupObservers(rootElement);
|
|
2575
|
+
} else {
|
|
2576
|
+
debug("overflow", `rootListener: root detached (ns=${editor.getKey()})`);
|
|
2577
|
+
if (observer) observer.disconnect();
|
|
2578
|
+
if (unregisterUpdateListener) unregisterUpdateListener();
|
|
2579
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2580
|
+
observer = null;
|
|
2581
|
+
unregisterUpdateListener = null;
|
|
2582
|
+
debounceTimer = null;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
);
|
|
2586
|
+
return () => {
|
|
2587
|
+
unregisterRootListener();
|
|
2588
|
+
if (observer) observer.disconnect();
|
|
2589
|
+
if (unregisterUpdateListener) unregisterUpdateListener();
|
|
2590
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2591
|
+
};
|
|
2592
|
+
}, [editor]);
|
|
2593
|
+
return null;
|
|
2594
|
+
};
|
|
2595
|
+
function createPageLabel(region, pageNumber) {
|
|
2596
|
+
if (region === "body") {
|
|
2597
|
+
return pageNumber ? `Page ${pageNumber}` : "Body";
|
|
2598
|
+
}
|
|
2599
|
+
const regionLabel = region.charAt(0).toUpperCase() + region.slice(1);
|
|
2600
|
+
return pageNumber ? `${regionLabel} Page ${pageNumber}` : regionLabel;
|
|
2601
|
+
}
|
|
2602
|
+
const HistoryCapturePlugin = ({ pageId, region }) => {
|
|
2603
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2604
|
+
const { document: document2, queueHistoryAction } = useDocument();
|
|
2605
|
+
const context = React.useMemo(() => {
|
|
2606
|
+
const pageNumber = document2.pages.findIndex((page) => page.id === pageId);
|
|
2607
|
+
return {
|
|
2608
|
+
pageNumber: pageNumber >= 0 ? pageNumber + 1 : null,
|
|
2609
|
+
source: region
|
|
2610
|
+
};
|
|
2611
|
+
}, [document2.pages, pageId, region]);
|
|
2612
|
+
React.useEffect(() => {
|
|
2613
|
+
const buildDescriptor = (prefix) => ({
|
|
2614
|
+
label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
|
|
2615
|
+
source: context.source,
|
|
2616
|
+
pageId,
|
|
2617
|
+
region
|
|
2618
|
+
});
|
|
2619
|
+
return editor.registerCommand(
|
|
2620
|
+
lexical.CONTROLLED_TEXT_INSERTION_COMMAND,
|
|
2621
|
+
() => {
|
|
2622
|
+
queueHistoryAction(buildDescriptor("Typed text"));
|
|
2623
|
+
return false;
|
|
2624
|
+
},
|
|
2625
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
2626
|
+
);
|
|
2627
|
+
}, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
|
|
2628
|
+
React.useEffect(() => {
|
|
2629
|
+
const buildDescriptor = (prefix) => ({
|
|
2630
|
+
label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
|
|
2631
|
+
source: context.source,
|
|
2632
|
+
pageId,
|
|
2633
|
+
region
|
|
2634
|
+
});
|
|
2635
|
+
return editor.registerCommand(
|
|
2636
|
+
lexical.KEY_DOWN_COMMAND,
|
|
2637
|
+
(event) => {
|
|
2638
|
+
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
2639
|
+
return false;
|
|
2640
|
+
}
|
|
2641
|
+
if (event.key.length !== 1) {
|
|
2642
|
+
return false;
|
|
2643
|
+
}
|
|
2644
|
+
queueHistoryAction(buildDescriptor("Typed text"));
|
|
2645
|
+
return false;
|
|
2646
|
+
},
|
|
2647
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
2648
|
+
);
|
|
2649
|
+
}, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
|
|
2650
|
+
React.useEffect(() => {
|
|
2651
|
+
const buildDescriptor = (prefix) => ({
|
|
2652
|
+
label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
|
|
2653
|
+
source: context.source,
|
|
2654
|
+
pageId,
|
|
2655
|
+
region
|
|
2656
|
+
});
|
|
2657
|
+
return editor.registerCommand(
|
|
2658
|
+
lexical.PASTE_COMMAND,
|
|
2659
|
+
(event) => {
|
|
2660
|
+
var _a;
|
|
2661
|
+
const text = ((_a = event.clipboardData) == null ? void 0 : _a.getData("text/plain")) ?? "";
|
|
2662
|
+
if (text.trim().length === 0) {
|
|
2663
|
+
return false;
|
|
2664
|
+
}
|
|
2665
|
+
queueHistoryAction(buildDescriptor("Pasted content"));
|
|
2666
|
+
return false;
|
|
2667
|
+
},
|
|
2668
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
2669
|
+
);
|
|
2670
|
+
}, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
|
|
2671
|
+
React.useEffect(() => {
|
|
2672
|
+
const buildDescriptor = (prefix) => ({
|
|
2673
|
+
label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
|
|
2674
|
+
source: context.source,
|
|
2675
|
+
pageId,
|
|
2676
|
+
region
|
|
2677
|
+
});
|
|
2678
|
+
return editor.registerCommand(
|
|
2679
|
+
lexical.KEY_ENTER_COMMAND,
|
|
2680
|
+
() => {
|
|
2681
|
+
queueHistoryAction(buildDescriptor("Inserted line break"));
|
|
2682
|
+
return false;
|
|
2683
|
+
},
|
|
2684
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
2685
|
+
);
|
|
2686
|
+
}, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
|
|
2687
|
+
React.useEffect(() => {
|
|
2688
|
+
const buildDescriptor = (prefix) => ({
|
|
2689
|
+
label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
|
|
2690
|
+
source: context.source,
|
|
2691
|
+
pageId,
|
|
2692
|
+
region
|
|
2693
|
+
});
|
|
2694
|
+
return editor.registerCommand(
|
|
2695
|
+
lexical.KEY_BACKSPACE_COMMAND,
|
|
2696
|
+
() => {
|
|
2697
|
+
queueHistoryAction(buildDescriptor("Deleted backward"));
|
|
2698
|
+
return false;
|
|
2699
|
+
},
|
|
2700
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
2701
|
+
);
|
|
2702
|
+
}, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
|
|
2703
|
+
React.useEffect(() => {
|
|
2704
|
+
const buildDescriptor = (prefix) => ({
|
|
2705
|
+
label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
|
|
2706
|
+
source: context.source,
|
|
2707
|
+
pageId,
|
|
2708
|
+
region
|
|
2709
|
+
});
|
|
2710
|
+
return editor.registerCommand(
|
|
2711
|
+
lexical.KEY_DELETE_COMMAND,
|
|
2712
|
+
() => {
|
|
2713
|
+
queueHistoryAction(buildDescriptor("Deleted forward"));
|
|
2714
|
+
return false;
|
|
2715
|
+
},
|
|
2716
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
2717
|
+
);
|
|
2718
|
+
}, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
|
|
2719
|
+
React.useEffect(() => {
|
|
2720
|
+
const buildDescriptor = (prefix) => ({
|
|
2721
|
+
label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
|
|
2722
|
+
source: context.source,
|
|
2723
|
+
pageId,
|
|
2724
|
+
region
|
|
2725
|
+
});
|
|
2726
|
+
return editor.registerCommand(
|
|
2727
|
+
lexical.FORMAT_TEXT_COMMAND,
|
|
2728
|
+
() => {
|
|
2729
|
+
queueHistoryAction(buildDescriptor("Formatted text"));
|
|
2730
|
+
return false;
|
|
2731
|
+
},
|
|
2732
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
2733
|
+
);
|
|
2734
|
+
}, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
|
|
2735
|
+
React.useEffect(() => {
|
|
2736
|
+
const buildDescriptor = (prefix) => ({
|
|
2737
|
+
label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
|
|
2738
|
+
source: context.source,
|
|
2739
|
+
pageId,
|
|
2740
|
+
region
|
|
2741
|
+
});
|
|
2742
|
+
return editor.registerCommand(
|
|
2743
|
+
lexical.FORMAT_ELEMENT_COMMAND,
|
|
2744
|
+
() => {
|
|
2745
|
+
queueHistoryAction(buildDescriptor("Formatted paragraph"));
|
|
2746
|
+
return false;
|
|
2747
|
+
},
|
|
2748
|
+
lexical.COMMAND_PRIORITY_LOW
|
|
2749
|
+
);
|
|
2750
|
+
}, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
|
|
2751
|
+
return null;
|
|
2752
|
+
};
|
|
2753
|
+
function getCollapsedTextPosition(rootElement) {
|
|
2754
|
+
const domSelection = window.getSelection();
|
|
2755
|
+
if (!rootElement || !domSelection || domSelection.rangeCount === 0 || !domSelection.isCollapsed) {
|
|
2756
|
+
return null;
|
|
2757
|
+
}
|
|
2758
|
+
const range = domSelection.getRangeAt(0);
|
|
2759
|
+
if (!rootElement.contains(range.startContainer)) {
|
|
2760
|
+
return null;
|
|
2761
|
+
}
|
|
2762
|
+
const offsetRange = document.createRange();
|
|
2763
|
+
offsetRange.setStart(rootElement, 0);
|
|
2764
|
+
offsetRange.setEnd(range.startContainer, range.startOffset);
|
|
2765
|
+
const contentRange = document.createRange();
|
|
2766
|
+
contentRange.selectNodeContents(rootElement);
|
|
2767
|
+
return {
|
|
2768
|
+
textOffset: offsetRange.toString().length,
|
|
2769
|
+
totalTextLength: contentRange.toString().length
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
const PageBoundaryPlugin = ({
|
|
2773
|
+
onBackspaceAtStart,
|
|
2774
|
+
onDeleteAtEnd,
|
|
2775
|
+
onMoveToPreviousPage,
|
|
2776
|
+
onMoveToNextPage
|
|
2777
|
+
}) => {
|
|
2778
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2779
|
+
React.useEffect(() => editor.registerCommand(
|
|
2780
|
+
lexical.KEY_BACKSPACE_COMMAND,
|
|
2781
|
+
() => {
|
|
2782
|
+
const position = getCollapsedTextPosition(editor.getRootElement());
|
|
2783
|
+
if (!position || position.textOffset !== 0) {
|
|
2784
|
+
return false;
|
|
2785
|
+
}
|
|
2786
|
+
onBackspaceAtStart();
|
|
2787
|
+
return true;
|
|
2788
|
+
},
|
|
2789
|
+
lexical.COMMAND_PRIORITY_HIGH
|
|
2790
|
+
), [editor, onBackspaceAtStart]);
|
|
2791
|
+
React.useEffect(() => editor.registerCommand(
|
|
2792
|
+
lexical.KEY_DELETE_COMMAND,
|
|
2793
|
+
() => {
|
|
2794
|
+
const position = getCollapsedTextPosition(editor.getRootElement());
|
|
2795
|
+
if (!position || position.textOffset !== position.totalTextLength) {
|
|
2796
|
+
return false;
|
|
2797
|
+
}
|
|
2798
|
+
onDeleteAtEnd();
|
|
2799
|
+
return true;
|
|
2800
|
+
},
|
|
2801
|
+
lexical.COMMAND_PRIORITY_HIGH
|
|
2802
|
+
), [editor, onDeleteAtEnd]);
|
|
2803
|
+
React.useEffect(() => editor.registerCommand(
|
|
2804
|
+
lexical.KEY_DOWN_COMMAND,
|
|
2805
|
+
(event) => {
|
|
2806
|
+
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) {
|
|
2807
|
+
return false;
|
|
2808
|
+
}
|
|
2809
|
+
const position = getCollapsedTextPosition(editor.getRootElement());
|
|
2810
|
+
if (!position) {
|
|
2811
|
+
return false;
|
|
2812
|
+
}
|
|
2813
|
+
if ((event.key === "ArrowLeft" || event.key === "ArrowUp") && position.textOffset === 0) {
|
|
2814
|
+
event.preventDefault();
|
|
2815
|
+
onMoveToPreviousPage();
|
|
2816
|
+
return true;
|
|
2817
|
+
}
|
|
2818
|
+
if ((event.key === "ArrowRight" || event.key === "ArrowDown") && position.textOffset === position.totalTextLength) {
|
|
2819
|
+
event.preventDefault();
|
|
2820
|
+
onMoveToNextPage();
|
|
2821
|
+
return true;
|
|
2822
|
+
}
|
|
2823
|
+
return false;
|
|
2824
|
+
},
|
|
2825
|
+
lexical.COMMAND_PRIORITY_HIGH
|
|
2826
|
+
), [editor, onMoveToNextPage, onMoveToPreviousPage]);
|
|
2827
|
+
return null;
|
|
2828
|
+
};
|
|
2829
|
+
const EditorRegistryPlugin = ({ pageId }) => {
|
|
2830
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2831
|
+
const { editorRegistry } = useDocument();
|
|
2832
|
+
React.useEffect(() => {
|
|
2833
|
+
editorRegistry.register(pageId, editor);
|
|
2834
|
+
debug("registry", `registered editor for page ${shortId(pageId)}`);
|
|
2835
|
+
return () => {
|
|
2836
|
+
editorRegistry.unregister(pageId);
|
|
2837
|
+
debug("registry", `unregistered editor for page ${shortId(pageId)}`);
|
|
2838
|
+
};
|
|
2839
|
+
}, [editor, pageId, editorRegistry]);
|
|
2840
|
+
return null;
|
|
2841
|
+
};
|
|
2842
|
+
const PageBody = ({
|
|
2843
|
+
pageId,
|
|
2844
|
+
initialBodyState,
|
|
2845
|
+
onBodyChange,
|
|
2846
|
+
onOverflow,
|
|
2847
|
+
onFocus,
|
|
2848
|
+
onBackspaceAtStart,
|
|
2849
|
+
onDeleteAtEnd,
|
|
2850
|
+
onMoveToPreviousPage,
|
|
2851
|
+
onMoveToNextPage,
|
|
2852
|
+
readOnly = false
|
|
2853
|
+
}) => {
|
|
2854
|
+
const config = React.useMemo(
|
|
2855
|
+
() => {
|
|
2856
|
+
var _a, _b;
|
|
2857
|
+
const baseConfig = {
|
|
2858
|
+
...createEditorConfig("body", pageId),
|
|
2859
|
+
editable: !readOnly
|
|
2860
|
+
};
|
|
2861
|
+
if (initialBodyState) {
|
|
2862
|
+
debug("page", `PageBody ${shortId(pageId)}: initializing with ${((_b = (_a = initialBodyState.root) == null ? void 0 : _a.children) == null ? void 0 : _b.length) ?? 0} children`);
|
|
2863
|
+
return {
|
|
2864
|
+
...baseConfig,
|
|
2865
|
+
editorState: JSON.stringify(initialBodyState)
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
debug("page", `PageBody ${shortId(pageId)}: initializing empty`);
|
|
2869
|
+
return baseConfig;
|
|
2870
|
+
},
|
|
2871
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally only use initialBodyState at mount time
|
|
2872
|
+
[pageId, readOnly]
|
|
2873
|
+
);
|
|
2874
|
+
const handleChange = React.useCallback(
|
|
2875
|
+
(editorState) => {
|
|
2876
|
+
const serialized = editorState.toJSON();
|
|
2877
|
+
onBodyChange == null ? void 0 : onBodyChange(serialized);
|
|
2878
|
+
},
|
|
2879
|
+
[onBodyChange]
|
|
2880
|
+
);
|
|
2881
|
+
const handleOverflow = React.useCallback(
|
|
2882
|
+
(overflowContent, cause) => {
|
|
2883
|
+
var _a, _b;
|
|
2884
|
+
debug("page", `PageBody ${shortId(pageId)}: overflow callback fired with ${((_b = (_a = overflowContent.root) == null ? void 0 : _a.children) == null ? void 0 : _b.length) ?? 0} children`);
|
|
2885
|
+
onOverflow == null ? void 0 : onOverflow(overflowContent, cause);
|
|
2886
|
+
},
|
|
2887
|
+
[onOverflow, pageId]
|
|
2888
|
+
);
|
|
2889
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2890
|
+
"div",
|
|
2891
|
+
{
|
|
2892
|
+
className: "lex4-page-body flex-1 min-h-0 relative",
|
|
2893
|
+
style: { overflow: "hidden" },
|
|
2894
|
+
"data-testid": `page-body-${pageId}`,
|
|
2895
|
+
onFocus,
|
|
2896
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(LexicalComposer.LexicalComposer, { initialConfig: config, children: [
|
|
2897
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2898
|
+
LexicalRichTextPlugin.RichTextPlugin,
|
|
2899
|
+
{
|
|
2900
|
+
contentEditable: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2901
|
+
LexicalContentEditable.ContentEditable,
|
|
2902
|
+
{
|
|
2903
|
+
className: "outline-none h-full p-0",
|
|
2904
|
+
style: { overflow: "visible" }
|
|
2905
|
+
}
|
|
2906
|
+
),
|
|
2907
|
+
placeholder: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-0 left-0 text-gray-400 pointer-events-none select-none", children: "Start typing..." }),
|
|
2908
|
+
ErrorBoundary: LexicalErrorBoundary.LexicalErrorBoundary
|
|
2909
|
+
}
|
|
2910
|
+
),
|
|
2911
|
+
/* @__PURE__ */ jsxRuntime.jsx(LexicalListPlugin.ListPlugin, {}),
|
|
2912
|
+
/* @__PURE__ */ jsxRuntime.jsx(ParagraphIndentPlugin, {}),
|
|
2913
|
+
/* @__PURE__ */ jsxRuntime.jsx(TabIndentPlugin, {}),
|
|
2914
|
+
/* @__PURE__ */ jsxRuntime.jsx(PastePlugin, {}),
|
|
2915
|
+
/* @__PURE__ */ jsxRuntime.jsx(EditorRegistryPlugin, { pageId }),
|
|
2916
|
+
/* @__PURE__ */ jsxRuntime.jsx(ActiveEditorPlugin, { pageId, region: "body" }),
|
|
2917
|
+
!readOnly && /* @__PURE__ */ jsxRuntime.jsx(HistoryCapturePlugin, { pageId, region: "body" }),
|
|
2918
|
+
!readOnly && onBackspaceAtStart && onDeleteAtEnd && onMoveToPreviousPage && onMoveToNextPage && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2919
|
+
PageBoundaryPlugin,
|
|
2920
|
+
{
|
|
2921
|
+
onBackspaceAtStart,
|
|
2922
|
+
onDeleteAtEnd,
|
|
2923
|
+
onMoveToPreviousPage,
|
|
2924
|
+
onMoveToNextPage
|
|
2925
|
+
}
|
|
2926
|
+
),
|
|
2927
|
+
/* @__PURE__ */ jsxRuntime.jsx(OverflowPlugin, { onOverflow: handleOverflow }),
|
|
2928
|
+
/* @__PURE__ */ jsxRuntime.jsx(LexicalOnChangePlugin.OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true })
|
|
2929
|
+
] })
|
|
2930
|
+
}
|
|
2931
|
+
);
|
|
2932
|
+
};
|
|
2933
|
+
function HeightLimitPlugin({ maxHeight, channel }) {
|
|
2934
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
2935
|
+
const lastGoodStateRef = React.useRef(null);
|
|
2936
|
+
const isRevertingRef = React.useRef(false);
|
|
2937
|
+
React.useEffect(() => {
|
|
2938
|
+
lastGoodStateRef.current = JSON.stringify(editor.getEditorState().toJSON());
|
|
2939
|
+
const removeEnterCommand = editor.registerCommand(
|
|
2940
|
+
lexical.KEY_ENTER_COMMAND,
|
|
2941
|
+
() => {
|
|
2942
|
+
const root = editor.getRootElement();
|
|
2943
|
+
if (root && root.scrollHeight >= maxHeight) {
|
|
2944
|
+
debug(channel, `blocked Enter — scrollHeight ${root.scrollHeight}px >= max ${maxHeight}px`);
|
|
2945
|
+
return true;
|
|
2946
|
+
}
|
|
2947
|
+
return false;
|
|
2948
|
+
},
|
|
2949
|
+
lexical.COMMAND_PRIORITY_CRITICAL
|
|
2950
|
+
);
|
|
2951
|
+
const removePasteCommand = editor.registerCommand(
|
|
2952
|
+
lexical.PASTE_COMMAND,
|
|
2953
|
+
() => {
|
|
2954
|
+
const root = editor.getRootElement();
|
|
2955
|
+
if (root && root.scrollHeight >= maxHeight) {
|
|
2956
|
+
debug(channel, `blocked paste — scrollHeight ${root.scrollHeight}px >= max ${maxHeight}px`);
|
|
2957
|
+
return true;
|
|
2958
|
+
}
|
|
2959
|
+
return false;
|
|
2960
|
+
},
|
|
2961
|
+
lexical.COMMAND_PRIORITY_CRITICAL
|
|
2962
|
+
);
|
|
2963
|
+
const removeUpdateListener = editor.registerUpdateListener(({ editorState, prevEditorState }) => {
|
|
2964
|
+
if (isRevertingRef.current) return;
|
|
2965
|
+
const root = editor.getRootElement();
|
|
2966
|
+
if (!root) return;
|
|
2967
|
+
requestAnimationFrame(() => {
|
|
2968
|
+
if (root.scrollHeight > maxHeight) {
|
|
2969
|
+
debug(channel, `reverting — scrollHeight ${root.scrollHeight}px > max ${maxHeight}px`);
|
|
2970
|
+
const restoreState = JSON.stringify(prevEditorState.toJSON()) || lastGoodStateRef.current;
|
|
2971
|
+
if (!restoreState) {
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
isRevertingRef.current = true;
|
|
2975
|
+
editor.setEditorState(editor.parseEditorState(restoreState));
|
|
2976
|
+
isRevertingRef.current = false;
|
|
2977
|
+
} else {
|
|
2978
|
+
lastGoodStateRef.current = JSON.stringify(editorState.toJSON());
|
|
2979
|
+
}
|
|
2980
|
+
});
|
|
2981
|
+
});
|
|
2982
|
+
return () => {
|
|
2983
|
+
removeEnterCommand();
|
|
2984
|
+
removePasteCommand();
|
|
2985
|
+
removeUpdateListener();
|
|
2986
|
+
};
|
|
2987
|
+
}, [editor, maxHeight, channel]);
|
|
2988
|
+
return null;
|
|
2989
|
+
}
|
|
2990
|
+
const PageHeader = ({
|
|
2991
|
+
pageId,
|
|
2992
|
+
initialHeaderState,
|
|
2993
|
+
pageCounterLabel,
|
|
2994
|
+
onHeaderChange
|
|
2995
|
+
}) => {
|
|
2996
|
+
const hasPageCounter = !!pageCounterLabel;
|
|
2997
|
+
const config = React.useMemo(
|
|
2998
|
+
() => {
|
|
2999
|
+
const baseConfig = createEditorConfig("header", pageId);
|
|
3000
|
+
if (initialHeaderState) {
|
|
3001
|
+
return { ...baseConfig, editorState: JSON.stringify(initialHeaderState) };
|
|
3002
|
+
}
|
|
3003
|
+
return baseConfig;
|
|
3004
|
+
},
|
|
3005
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- syncVersion forces remount via key, only used at init
|
|
3006
|
+
[pageId]
|
|
3007
|
+
);
|
|
3008
|
+
const contentRef = React.useRef(null);
|
|
3009
|
+
const handleChange = React.useCallback(
|
|
3010
|
+
(editorState) => {
|
|
3011
|
+
requestAnimationFrame(() => {
|
|
3012
|
+
const el = contentRef.current;
|
|
3013
|
+
if (el) {
|
|
3014
|
+
if (el.scrollHeight > MAX_HEADER_HEIGHT_PX) {
|
|
3015
|
+
debug(
|
|
3016
|
+
"header",
|
|
3017
|
+
`page ${shortId(pageId)}: skipped external sync at ${el.scrollHeight}px > max ${MAX_HEADER_HEIGHT_PX}px`
|
|
3018
|
+
);
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
const height = Math.min(el.scrollHeight, MAX_HEADER_HEIGHT_PX);
|
|
3022
|
+
debug("header", `page ${shortId(pageId)}: height=${height}px (scrollH=${el.scrollHeight})`);
|
|
3023
|
+
onHeaderChange == null ? void 0 : onHeaderChange(editorState.toJSON(), height);
|
|
3024
|
+
}
|
|
3025
|
+
});
|
|
3026
|
+
},
|
|
3027
|
+
[onHeaderChange, pageId]
|
|
3028
|
+
);
|
|
3029
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3030
|
+
"div",
|
|
3031
|
+
{
|
|
3032
|
+
className: "lex4-page-header border-b border-dashed border-gray-200 relative flex-shrink-0",
|
|
3033
|
+
style: { maxHeight: MAX_HEADER_HEIGHT_PX, overflow: "clip" },
|
|
3034
|
+
"data-testid": `page-header-${pageId}`,
|
|
3035
|
+
children: [
|
|
3036
|
+
/* @__PURE__ */ jsxRuntime.jsxs(LexicalComposer.LexicalComposer, { initialConfig: config, children: [
|
|
3037
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3038
|
+
LexicalRichTextPlugin.RichTextPlugin,
|
|
3039
|
+
{
|
|
3040
|
+
contentEditable: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3041
|
+
LexicalContentEditable.ContentEditable,
|
|
3042
|
+
{
|
|
3043
|
+
ref: contentRef,
|
|
3044
|
+
className: `outline-none p-2 text-sm text-gray-600 min-h-[24px] ${hasPageCounter ? "pr-24" : ""}`
|
|
3045
|
+
}
|
|
3046
|
+
),
|
|
3047
|
+
placeholder: /* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute top-0 left-0 text-gray-400 pointer-events-none select-none p-2 text-sm ${hasPageCounter ? "pr-24" : ""}`, children: "Header" }),
|
|
3048
|
+
ErrorBoundary: LexicalErrorBoundary.LexicalErrorBoundary
|
|
3049
|
+
}
|
|
3050
|
+
),
|
|
3051
|
+
/* @__PURE__ */ jsxRuntime.jsx(ActiveEditorPlugin, { pageId, region: "header" }),
|
|
3052
|
+
/* @__PURE__ */ jsxRuntime.jsx(HistoryCapturePlugin, { pageId, region: "header" }),
|
|
3053
|
+
/* @__PURE__ */ jsxRuntime.jsx(HeightLimitPlugin, { maxHeight: MAX_HEADER_HEIGHT_PX, channel: "header" }),
|
|
3054
|
+
/* @__PURE__ */ jsxRuntime.jsx(LexicalOnChangePlugin.OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true })
|
|
3055
|
+
] }),
|
|
3056
|
+
pageCounterLabel && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3057
|
+
"div",
|
|
3058
|
+
{
|
|
3059
|
+
className: "pointer-events-none absolute right-2 top-2 select-none text-xs text-gray-500",
|
|
3060
|
+
"data-testid": `page-counter-header-${pageId}`,
|
|
3061
|
+
children: pageCounterLabel
|
|
3062
|
+
}
|
|
3063
|
+
)
|
|
3064
|
+
]
|
|
3065
|
+
}
|
|
3066
|
+
);
|
|
3067
|
+
};
|
|
3068
|
+
const PageFooter = ({
|
|
3069
|
+
pageId,
|
|
3070
|
+
initialFooterState,
|
|
3071
|
+
pageCounterLabel,
|
|
3072
|
+
onFooterChange
|
|
3073
|
+
}) => {
|
|
3074
|
+
const hasPageCounter = !!pageCounterLabel;
|
|
3075
|
+
const config = React.useMemo(
|
|
3076
|
+
() => {
|
|
3077
|
+
const baseConfig = createEditorConfig("footer", pageId);
|
|
3078
|
+
if (initialFooterState) {
|
|
3079
|
+
return { ...baseConfig, editorState: JSON.stringify(initialFooterState) };
|
|
3080
|
+
}
|
|
3081
|
+
return baseConfig;
|
|
3082
|
+
},
|
|
3083
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- syncVersion forces remount via key, only used at init
|
|
3084
|
+
[pageId]
|
|
3085
|
+
);
|
|
3086
|
+
const contentRef = React.useRef(null);
|
|
3087
|
+
const handleChange = React.useCallback(
|
|
3088
|
+
(editorState) => {
|
|
3089
|
+
requestAnimationFrame(() => {
|
|
3090
|
+
const el = contentRef.current;
|
|
3091
|
+
if (el) {
|
|
3092
|
+
if (el.scrollHeight > MAX_FOOTER_HEIGHT_PX) {
|
|
3093
|
+
debug(
|
|
3094
|
+
"footer",
|
|
3095
|
+
`page ${shortId(pageId)}: skipped external sync at ${el.scrollHeight}px > max ${MAX_FOOTER_HEIGHT_PX}px`
|
|
3096
|
+
);
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
const height = Math.min(el.scrollHeight, MAX_FOOTER_HEIGHT_PX);
|
|
3100
|
+
debug("footer", `page ${shortId(pageId)}: height=${height}px (scrollH=${el.scrollHeight})`);
|
|
3101
|
+
onFooterChange == null ? void 0 : onFooterChange(editorState.toJSON(), height);
|
|
3102
|
+
}
|
|
3103
|
+
});
|
|
3104
|
+
},
|
|
3105
|
+
[onFooterChange, pageId]
|
|
3106
|
+
);
|
|
3107
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3108
|
+
"div",
|
|
3109
|
+
{
|
|
3110
|
+
className: "lex4-page-footer border-t border-dashed border-gray-200 relative flex-shrink-0",
|
|
3111
|
+
style: { maxHeight: MAX_FOOTER_HEIGHT_PX, overflow: "clip" },
|
|
3112
|
+
"data-testid": `page-footer-${pageId}`,
|
|
3113
|
+
children: [
|
|
3114
|
+
/* @__PURE__ */ jsxRuntime.jsxs(LexicalComposer.LexicalComposer, { initialConfig: config, children: [
|
|
3115
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3116
|
+
LexicalRichTextPlugin.RichTextPlugin,
|
|
3117
|
+
{
|
|
3118
|
+
contentEditable: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3119
|
+
LexicalContentEditable.ContentEditable,
|
|
3120
|
+
{
|
|
3121
|
+
ref: contentRef,
|
|
3122
|
+
className: `outline-none p-2 text-sm text-gray-600 min-h-[24px] ${hasPageCounter ? "pr-24" : ""}`
|
|
3123
|
+
}
|
|
3124
|
+
),
|
|
3125
|
+
placeholder: /* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute top-0 left-0 text-gray-400 pointer-events-none select-none p-2 text-sm ${hasPageCounter ? "pr-24" : ""}`, children: "Footer" }),
|
|
3126
|
+
ErrorBoundary: LexicalErrorBoundary.LexicalErrorBoundary
|
|
3127
|
+
}
|
|
3128
|
+
),
|
|
3129
|
+
/* @__PURE__ */ jsxRuntime.jsx(ActiveEditorPlugin, { pageId, region: "footer" }),
|
|
3130
|
+
/* @__PURE__ */ jsxRuntime.jsx(HistoryCapturePlugin, { pageId, region: "footer" }),
|
|
3131
|
+
/* @__PURE__ */ jsxRuntime.jsx(HeightLimitPlugin, { maxHeight: MAX_FOOTER_HEIGHT_PX, channel: "footer" }),
|
|
3132
|
+
/* @__PURE__ */ jsxRuntime.jsx(LexicalOnChangePlugin.OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true })
|
|
3133
|
+
] }),
|
|
3134
|
+
pageCounterLabel && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3135
|
+
"div",
|
|
3136
|
+
{
|
|
3137
|
+
className: "pointer-events-none absolute right-2 top-2 select-none text-xs text-gray-500",
|
|
3138
|
+
"data-testid": `page-counter-footer-${pageId}`,
|
|
3139
|
+
children: pageCounterLabel
|
|
3140
|
+
}
|
|
3141
|
+
)
|
|
3142
|
+
]
|
|
3143
|
+
}
|
|
3144
|
+
);
|
|
3145
|
+
};
|
|
3146
|
+
const PageView = React.memo(({
|
|
3147
|
+
pageId,
|
|
3148
|
+
pageIndex,
|
|
3149
|
+
onOverflow,
|
|
3150
|
+
onBackspaceAtStart,
|
|
3151
|
+
onDeleteAtEnd,
|
|
3152
|
+
onMoveToPreviousPage,
|
|
3153
|
+
onMoveToNextPage
|
|
3154
|
+
}) => {
|
|
3155
|
+
const { document: document2, dispatch, setActivePageId } = useDocument();
|
|
3156
|
+
const page = document2.pages.find((p) => p.id === pageId);
|
|
3157
|
+
const showHeaderFooter = document2.headerFooterEnabled;
|
|
3158
|
+
const pageCounterMode = document2.pageCounterMode;
|
|
3159
|
+
const pageCounterLabel = `Page ${pageIndex + 1} of ${document2.pages.length}`;
|
|
3160
|
+
if (!page) return null;
|
|
3161
|
+
const handleBodyChange = React.useCallback(
|
|
3162
|
+
(bodyState) => {
|
|
3163
|
+
dispatch({ type: "UPDATE_PAGE_BODY", pageId, bodyState });
|
|
3164
|
+
},
|
|
3165
|
+
[dispatch, pageId]
|
|
3166
|
+
);
|
|
3167
|
+
const handleHeaderChange = React.useCallback(
|
|
3168
|
+
(headerState, height) => {
|
|
3169
|
+
dispatch({ type: "UPDATE_PAGE_HEADER_CONTENT", pageId, headerState, height });
|
|
3170
|
+
},
|
|
3171
|
+
[dispatch, pageId]
|
|
3172
|
+
);
|
|
3173
|
+
const handleFooterChange = React.useCallback(
|
|
3174
|
+
(footerState, height) => {
|
|
3175
|
+
dispatch({ type: "UPDATE_PAGE_FOOTER_CONTENT", pageId, footerState, height });
|
|
3176
|
+
},
|
|
3177
|
+
[dispatch, pageId]
|
|
3178
|
+
);
|
|
3179
|
+
const handleFocus = React.useCallback(() => {
|
|
3180
|
+
setActivePageId(pageId);
|
|
3181
|
+
}, [setActivePageId, pageId]);
|
|
3182
|
+
const handleOverflow = React.useCallback(
|
|
3183
|
+
(overflowContent, cause) => {
|
|
3184
|
+
onOverflow == null ? void 0 : onOverflow(overflowContent, cause);
|
|
3185
|
+
},
|
|
3186
|
+
[onOverflow]
|
|
3187
|
+
);
|
|
3188
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3189
|
+
"div",
|
|
3190
|
+
{
|
|
3191
|
+
className: "lex4-page bg-white shadow-sm flex flex-col",
|
|
3192
|
+
style: {
|
|
3193
|
+
width: A4_WIDTH_PX,
|
|
3194
|
+
height: A4_HEIGHT_PX,
|
|
3195
|
+
padding: PAGE_MARGIN_PX
|
|
3196
|
+
},
|
|
3197
|
+
"data-testid": `page-${pageIndex}`,
|
|
3198
|
+
"data-page-id": pageId,
|
|
3199
|
+
children: [
|
|
3200
|
+
showHeaderFooter && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3201
|
+
PageHeader,
|
|
3202
|
+
{
|
|
3203
|
+
pageId,
|
|
3204
|
+
initialHeaderState: page.headerState,
|
|
3205
|
+
pageCounterLabel: pageCounterMode === "header" || pageCounterMode === "both" ? pageCounterLabel : void 0,
|
|
3206
|
+
onHeaderChange: handleHeaderChange
|
|
3207
|
+
},
|
|
3208
|
+
`header-${page.headerSyncVersion}`
|
|
3209
|
+
),
|
|
3210
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3211
|
+
PageBody,
|
|
3212
|
+
{
|
|
3213
|
+
pageId,
|
|
3214
|
+
initialBodyState: page.bodyState,
|
|
3215
|
+
onBodyChange: handleBodyChange,
|
|
3216
|
+
onOverflow: handleOverflow,
|
|
3217
|
+
onFocus: handleFocus,
|
|
3218
|
+
onBackspaceAtStart: () => onBackspaceAtStart == null ? void 0 : onBackspaceAtStart(pageIndex, pageId),
|
|
3219
|
+
onDeleteAtEnd: () => onDeleteAtEnd == null ? void 0 : onDeleteAtEnd(pageIndex, pageId),
|
|
3220
|
+
onMoveToPreviousPage: () => onMoveToPreviousPage == null ? void 0 : onMoveToPreviousPage(pageIndex),
|
|
3221
|
+
onMoveToNextPage: () => onMoveToNextPage == null ? void 0 : onMoveToNextPage(pageIndex)
|
|
3222
|
+
},
|
|
3223
|
+
`body-${page.bodySyncVersion}`
|
|
3224
|
+
),
|
|
3225
|
+
showHeaderFooter && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3226
|
+
PageFooter,
|
|
3227
|
+
{
|
|
3228
|
+
pageId,
|
|
3229
|
+
initialFooterState: page.footerState,
|
|
3230
|
+
pageCounterLabel: pageCounterMode === "footer" || pageCounterMode === "both" ? pageCounterLabel : void 0,
|
|
3231
|
+
onFooterChange: handleFooterChange
|
|
3232
|
+
},
|
|
3233
|
+
`footer-${page.footerSyncVersion}`
|
|
3234
|
+
)
|
|
3235
|
+
]
|
|
3236
|
+
}
|
|
3237
|
+
);
|
|
3238
|
+
});
|
|
3239
|
+
PageView.displayName = "PageView";
|
|
3240
|
+
const DocumentView = () => {
|
|
3241
|
+
const {
|
|
3242
|
+
document: document2,
|
|
3243
|
+
dispatch,
|
|
3244
|
+
editorRegistry,
|
|
3245
|
+
requestFocusAtEnd,
|
|
3246
|
+
runHistoryAction,
|
|
3247
|
+
setActiveEditor,
|
|
3248
|
+
setActivePageId
|
|
3249
|
+
} = useDocument();
|
|
3250
|
+
const { handlePageUnderflow, reflowAll } = usePagination(document2, dispatch);
|
|
3251
|
+
const previousBodyHeightsRef = React.useRef(null);
|
|
3252
|
+
const pasteOverflowSequenceRef = React.useRef(false);
|
|
3253
|
+
const pasteOverflowReleaseTimerRef = React.useRef(null);
|
|
3254
|
+
const defaultPageTemplate = React.useMemo(
|
|
3255
|
+
() => ({
|
|
3256
|
+
headerState: document2.defaultHeaderState,
|
|
3257
|
+
footerState: document2.defaultFooterState,
|
|
3258
|
+
headerHeight: document2.defaultHeaderHeight,
|
|
3259
|
+
footerHeight: document2.defaultFooterHeight
|
|
3260
|
+
}),
|
|
3261
|
+
[
|
|
3262
|
+
document2.defaultFooterHeight,
|
|
3263
|
+
document2.defaultFooterState,
|
|
3264
|
+
document2.defaultHeaderHeight,
|
|
3265
|
+
document2.defaultHeaderState
|
|
3266
|
+
]
|
|
3267
|
+
);
|
|
3268
|
+
const bodyHeights = React.useMemo(
|
|
3269
|
+
() => document2.pages.map((page) => computeBodyHeight(
|
|
3270
|
+
document2.headerFooterEnabled ? page.headerHeight : 0,
|
|
3271
|
+
document2.headerFooterEnabled ? page.footerHeight : 0
|
|
3272
|
+
)),
|
|
3273
|
+
[document2.headerFooterEnabled, document2.pages]
|
|
3274
|
+
);
|
|
3275
|
+
React.useEffect(() => {
|
|
3276
|
+
const previousBodyHeights = previousBodyHeightsRef.current;
|
|
3277
|
+
previousBodyHeightsRef.current = bodyHeights;
|
|
3278
|
+
if (!previousBodyHeights) {
|
|
3279
|
+
return;
|
|
3280
|
+
}
|
|
3281
|
+
const bodySpaceExpanded = bodyHeights.some((height, index) => {
|
|
3282
|
+
const previousHeight = previousBodyHeights[index];
|
|
3283
|
+
return previousHeight !== void 0 && height > previousHeight;
|
|
3284
|
+
});
|
|
3285
|
+
if (bodySpaceExpanded) {
|
|
3286
|
+
debug("page", "body space expanded — running full reflow");
|
|
3287
|
+
reflowAll();
|
|
3288
|
+
}
|
|
3289
|
+
}, [bodyHeights, reflowAll]);
|
|
3290
|
+
React.useEffect(
|
|
3291
|
+
() => () => {
|
|
3292
|
+
if (pasteOverflowReleaseTimerRef.current) {
|
|
3293
|
+
clearTimeout(pasteOverflowReleaseTimerRef.current);
|
|
3294
|
+
}
|
|
3295
|
+
},
|
|
3296
|
+
[]
|
|
3297
|
+
);
|
|
3298
|
+
const focusBodyEditor = React.useCallback(
|
|
3299
|
+
(pageId, boundary) => {
|
|
3300
|
+
let attempts = 0;
|
|
3301
|
+
const focusWhenReady = () => {
|
|
3302
|
+
const editor = editorRegistry.get(pageId);
|
|
3303
|
+
if (!editor) {
|
|
3304
|
+
if (attempts < 4) {
|
|
3305
|
+
attempts += 1;
|
|
3306
|
+
requestAnimationFrame(focusWhenReady);
|
|
3307
|
+
}
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
const caretPosition = { pageId, region: "body" };
|
|
3311
|
+
setActivePageId(pageId);
|
|
3312
|
+
setActiveEditor(editor, caretPosition);
|
|
3313
|
+
editor.focus(() => {
|
|
3314
|
+
editor.update(() => {
|
|
3315
|
+
if (boundary === "start") {
|
|
3316
|
+
lexical.$getRoot().selectStart();
|
|
3317
|
+
} else {
|
|
3318
|
+
lexical.$getRoot().selectEnd();
|
|
3319
|
+
}
|
|
3320
|
+
});
|
|
3321
|
+
});
|
|
3322
|
+
};
|
|
3323
|
+
requestAnimationFrame(focusWhenReady);
|
|
3324
|
+
},
|
|
3325
|
+
[editorRegistry, setActiveEditor, setActivePageId]
|
|
3326
|
+
);
|
|
3327
|
+
const handlePageOverflow = React.useCallback(
|
|
3328
|
+
(pageIndex, overflowContent, cause) => {
|
|
3329
|
+
var _a, _b, _c, _d;
|
|
3330
|
+
const nextPageIndex = pageIndex + 1;
|
|
3331
|
+
const overflowChildCount = ((_b = (_a = overflowContent.root) == null ? void 0 : _a.children) == null ? void 0 : _b.length) ?? 0;
|
|
3332
|
+
debug("page", `handlePageOverflow: pageIndex=${pageIndex} overflowChildren=${overflowChildCount} totalPages=${document2.pages.length}`);
|
|
3333
|
+
if (cause === "paste") {
|
|
3334
|
+
pasteOverflowSequenceRef.current = true;
|
|
3335
|
+
}
|
|
3336
|
+
if (pasteOverflowSequenceRef.current) {
|
|
3337
|
+
if (pasteOverflowReleaseTimerRef.current) {
|
|
3338
|
+
clearTimeout(pasteOverflowReleaseTimerRef.current);
|
|
3339
|
+
}
|
|
3340
|
+
pasteOverflowReleaseTimerRef.current = setTimeout(() => {
|
|
3341
|
+
pasteOverflowSequenceRef.current = false;
|
|
3342
|
+
pasteOverflowReleaseTimerRef.current = null;
|
|
3343
|
+
}, 800);
|
|
3344
|
+
}
|
|
3345
|
+
if (nextPageIndex < document2.pages.length) {
|
|
3346
|
+
const nextPage = document2.pages[nextPageIndex];
|
|
3347
|
+
const nextEditor = editorRegistry.get(nextPage.id);
|
|
3348
|
+
if (nextEditor) {
|
|
3349
|
+
const currentState = nextEditor.getEditorState().toJSON();
|
|
3350
|
+
const existingChildren = ((_c = currentState.root) == null ? void 0 : _c.children) ?? [];
|
|
3351
|
+
const overflowChildren = ((_d = overflowContent.root) == null ? void 0 : _d.children) ?? [];
|
|
3352
|
+
debug("page", `prepending ${overflowChildren.length} nodes to existing page ${shortId(nextPage.id)} (had ${existingChildren.length} children)`);
|
|
3353
|
+
const mergedState = {
|
|
3354
|
+
root: {
|
|
3355
|
+
...currentState.root,
|
|
3356
|
+
children: [...overflowChildren, ...existingChildren]
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
const newEditorState = nextEditor.parseEditorState(JSON.stringify(mergedState));
|
|
3360
|
+
nextEditor.setEditorState(newEditorState);
|
|
3361
|
+
if (pasteOverflowSequenceRef.current) {
|
|
3362
|
+
requestFocusAtEnd({ pageId: nextPage.id, region: "body" });
|
|
3363
|
+
}
|
|
3364
|
+
} else {
|
|
3365
|
+
debug("page", `editor not found in registry for page ${shortId(nextPage.id)} — falling back to ADD_PAGE`);
|
|
3366
|
+
const newPage = createPageFromTemplate(defaultPageTemplate);
|
|
3367
|
+
newPage.bodyState = overflowContent;
|
|
3368
|
+
runHistoryAction(
|
|
3369
|
+
{
|
|
3370
|
+
label: "Overflow created new page",
|
|
3371
|
+
source: "overflow",
|
|
3372
|
+
region: "document"
|
|
3373
|
+
},
|
|
3374
|
+
() => {
|
|
3375
|
+
dispatch({ type: "ADD_PAGE", afterIndex: pageIndex, page: newPage });
|
|
3376
|
+
}
|
|
3377
|
+
);
|
|
3378
|
+
if (pasteOverflowSequenceRef.current) {
|
|
3379
|
+
requestFocusAtEnd({ pageId: newPage.id, region: "body" });
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
} else {
|
|
3383
|
+
const newPage = createPageFromTemplate(defaultPageTemplate);
|
|
3384
|
+
newPage.bodyState = overflowContent;
|
|
3385
|
+
debug("page", `creating new page ${shortId(newPage.id)} with ${overflowChildCount} overflow children`);
|
|
3386
|
+
runHistoryAction(
|
|
3387
|
+
{
|
|
3388
|
+
label: "Overflow created new page",
|
|
3389
|
+
source: "overflow",
|
|
3390
|
+
region: "document"
|
|
3391
|
+
},
|
|
3392
|
+
() => {
|
|
3393
|
+
dispatch({ type: "ADD_PAGE", page: newPage });
|
|
3394
|
+
}
|
|
3395
|
+
);
|
|
3396
|
+
if (pasteOverflowSequenceRef.current) {
|
|
3397
|
+
requestFocusAtEnd({ pageId: newPage.id, region: "body" });
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
},
|
|
3401
|
+
[defaultPageTemplate, document2.pages, dispatch, editorRegistry, requestFocusAtEnd, runHistoryAction]
|
|
3402
|
+
);
|
|
3403
|
+
const handleBackspaceAtPageStart = React.useCallback(
|
|
3404
|
+
(pageIndex, pageId) => {
|
|
3405
|
+
if (pageIndex <= 0) {
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
const previousPage = document2.pages[pageIndex - 1];
|
|
3409
|
+
if (!previousPage) {
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
runHistoryAction(
|
|
3413
|
+
{
|
|
3414
|
+
label: `Deleted backward - Page ${pageIndex + 1}`,
|
|
3415
|
+
source: "body",
|
|
3416
|
+
pageId,
|
|
3417
|
+
region: "body"
|
|
3418
|
+
},
|
|
3419
|
+
() => {
|
|
3420
|
+
handlePageUnderflow(pageIndex - 1);
|
|
3421
|
+
}
|
|
3422
|
+
);
|
|
3423
|
+
focusBodyEditor(previousPage.id, "end");
|
|
3424
|
+
},
|
|
3425
|
+
[document2.pages, focusBodyEditor, handlePageUnderflow, runHistoryAction]
|
|
3426
|
+
);
|
|
3427
|
+
const handleDeleteAtPageEnd = React.useCallback(
|
|
3428
|
+
(pageIndex, pageId) => {
|
|
3429
|
+
const currentPage = document2.pages[pageIndex];
|
|
3430
|
+
const nextPage = document2.pages[pageIndex + 1];
|
|
3431
|
+
if (!currentPage || !nextPage) {
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
runHistoryAction(
|
|
3435
|
+
{
|
|
3436
|
+
label: `Deleted forward - Page ${pageIndex + 1}`,
|
|
3437
|
+
source: "body",
|
|
3438
|
+
pageId,
|
|
3439
|
+
region: "body"
|
|
3440
|
+
},
|
|
3441
|
+
() => {
|
|
3442
|
+
handlePageUnderflow(pageIndex);
|
|
3443
|
+
}
|
|
3444
|
+
);
|
|
3445
|
+
focusBodyEditor(currentPage.id, "end");
|
|
3446
|
+
},
|
|
3447
|
+
[document2.pages, focusBodyEditor, handlePageUnderflow, runHistoryAction]
|
|
3448
|
+
);
|
|
3449
|
+
const handleMoveToPreviousPage = React.useCallback(
|
|
3450
|
+
(pageIndex) => {
|
|
3451
|
+
if (pageIndex <= 0) {
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
const previousPage = document2.pages[pageIndex - 1];
|
|
3455
|
+
if (!previousPage) {
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
focusBodyEditor(previousPage.id, "end");
|
|
3459
|
+
},
|
|
3460
|
+
[document2.pages, focusBodyEditor]
|
|
3461
|
+
);
|
|
3462
|
+
const handleMoveToNextPage = React.useCallback(
|
|
3463
|
+
(pageIndex) => {
|
|
3464
|
+
const nextPage = document2.pages[pageIndex + 1];
|
|
3465
|
+
if (!nextPage) {
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
focusBodyEditor(nextPage.id, "start");
|
|
3469
|
+
},
|
|
3470
|
+
[document2.pages, focusBodyEditor]
|
|
3471
|
+
);
|
|
3472
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3473
|
+
"div",
|
|
3474
|
+
{
|
|
3475
|
+
className: "lex4-document-view flex flex-col items-center gap-8 py-8 min-h-full",
|
|
3476
|
+
"data-testid": "document-view",
|
|
3477
|
+
tabIndex: -1,
|
|
3478
|
+
children: document2.pages.map((page, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3479
|
+
PageView,
|
|
3480
|
+
{
|
|
3481
|
+
pageId: page.id,
|
|
3482
|
+
pageIndex: index,
|
|
3483
|
+
onOverflow: (content, cause) => handlePageOverflow(index, content, cause),
|
|
3484
|
+
onBackspaceAtStart: handleBackspaceAtPageStart,
|
|
3485
|
+
onDeleteAtEnd: handleDeleteAtPageEnd,
|
|
3486
|
+
onMoveToPreviousPage: handleMoveToPreviousPage,
|
|
3487
|
+
onMoveToNextPage: handleMoveToNextPage
|
|
3488
|
+
},
|
|
3489
|
+
page.id
|
|
3490
|
+
))
|
|
3491
|
+
}
|
|
3492
|
+
);
|
|
3493
|
+
};
|
|
3494
|
+
function selectEntireDocument(rootElement, selectionBuffer) {
|
|
3495
|
+
if (!rootElement || !selectionBuffer) {
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
const bodyRoots = Array.from(
|
|
3499
|
+
rootElement.querySelectorAll('[data-testid^="page-body-"] [data-lexical-editor="true"]')
|
|
3500
|
+
);
|
|
3501
|
+
if (bodyRoots.length === 0) {
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3504
|
+
selectionBuffer.value = bodyRoots.map((bodyRoot) => bodyRoot.innerText.trim()).filter(Boolean).join("\n");
|
|
3505
|
+
selectionBuffer.focus();
|
|
3506
|
+
selectionBuffer.select();
|
|
3507
|
+
}
|
|
3508
|
+
const GLOBAL_SELECTION_BACKGROUND = "rgb(191, 219, 254)";
|
|
3509
|
+
const GLOBAL_SELECTION_FOREGROUND = "rgb(30, 64, 175)";
|
|
3510
|
+
function isFormFieldTarget(target) {
|
|
3511
|
+
const element = target;
|
|
3512
|
+
const tagName = element == null ? void 0 : element.tagName;
|
|
3513
|
+
return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
|
|
3514
|
+
}
|
|
3515
|
+
const EditorChrome = ({
|
|
3516
|
+
captureHistoryShortcutsOnWindow,
|
|
3517
|
+
className
|
|
3518
|
+
}) => {
|
|
3519
|
+
const {
|
|
3520
|
+
document: document2,
|
|
3521
|
+
dispatch,
|
|
3522
|
+
globalSelectionActive,
|
|
3523
|
+
setGlobalSelectionActive,
|
|
3524
|
+
undo,
|
|
3525
|
+
redo
|
|
3526
|
+
} = useDocument();
|
|
3527
|
+
const rootRef = React.useRef(null);
|
|
3528
|
+
const selectionBufferRef = React.useRef(null);
|
|
3529
|
+
const clearGlobalSelection = React.useCallback(() => {
|
|
3530
|
+
var _a;
|
|
3531
|
+
setGlobalSelectionActive(false);
|
|
3532
|
+
(_a = selectionBufferRef.current) == null ? void 0 : _a.blur();
|
|
3533
|
+
}, [setGlobalSelectionActive]);
|
|
3534
|
+
const handleHistoryShortcut = React.useCallback((event) => {
|
|
3535
|
+
var _a, _b;
|
|
3536
|
+
const key = event.key.toLowerCase();
|
|
3537
|
+
if (key === "z") {
|
|
3538
|
+
event.preventDefault();
|
|
3539
|
+
(_a = event.stopPropagation) == null ? void 0 : _a.call(event);
|
|
3540
|
+
clearGlobalSelection();
|
|
3541
|
+
if (event.shiftKey) {
|
|
3542
|
+
redo();
|
|
3543
|
+
} else {
|
|
3544
|
+
undo();
|
|
3545
|
+
}
|
|
3546
|
+
return true;
|
|
3547
|
+
}
|
|
3548
|
+
if (key === "y") {
|
|
3549
|
+
event.preventDefault();
|
|
3550
|
+
(_b = event.stopPropagation) == null ? void 0 : _b.call(event);
|
|
3551
|
+
clearGlobalSelection();
|
|
3552
|
+
redo();
|
|
3553
|
+
return true;
|
|
3554
|
+
}
|
|
3555
|
+
return false;
|
|
3556
|
+
}, [clearGlobalSelection, redo, undo]);
|
|
3557
|
+
const handleKeyDownCapture = React.useCallback((event) => {
|
|
3558
|
+
const target = event.target;
|
|
3559
|
+
const isGlobalSelectionBufferTarget = target === selectionBufferRef.current;
|
|
3560
|
+
if (!isGlobalSelectionBufferTarget && isFormFieldTarget(target)) {
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
const key = event.key.toLowerCase();
|
|
3564
|
+
if (isGlobalSelectionBufferTarget && (key === "backspace" || key === "delete")) {
|
|
3565
|
+
event.preventDefault();
|
|
3566
|
+
event.stopPropagation();
|
|
3567
|
+
dispatch({ type: "CLEAR_DOCUMENT_CONTENT" });
|
|
3568
|
+
clearGlobalSelection();
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
const hasModifier = event.metaKey || event.ctrlKey;
|
|
3572
|
+
if (!hasModifier) {
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
if (key === "a") {
|
|
3576
|
+
event.preventDefault();
|
|
3577
|
+
event.stopPropagation();
|
|
3578
|
+
requestAnimationFrame(() => {
|
|
3579
|
+
const activeElement = window.document.activeElement;
|
|
3580
|
+
if (activeElement instanceof HTMLElement && activeElement.isContentEditable) {
|
|
3581
|
+
activeElement.blur();
|
|
3582
|
+
}
|
|
3583
|
+
selectEntireDocument(rootRef.current, selectionBufferRef.current);
|
|
3584
|
+
setGlobalSelectionActive(true);
|
|
3585
|
+
});
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3588
|
+
if (handleHistoryShortcut(event)) {
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
}, [
|
|
3592
|
+
dispatch,
|
|
3593
|
+
handleHistoryShortcut,
|
|
3594
|
+
setGlobalSelectionActive
|
|
3595
|
+
]);
|
|
3596
|
+
const handleMouseDownCapture = React.useCallback((event) => {
|
|
3597
|
+
if (!globalSelectionActive) {
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
const target = event.target;
|
|
3601
|
+
const clickedToolbar = target == null ? void 0 : target.closest('[data-testid="toolbar"]');
|
|
3602
|
+
if (event.target !== selectionBufferRef.current && !clickedToolbar) {
|
|
3603
|
+
clearGlobalSelection();
|
|
3604
|
+
}
|
|
3605
|
+
}, [clearGlobalSelection, globalSelectionActive]);
|
|
3606
|
+
React.useEffect(() => {
|
|
3607
|
+
var _a;
|
|
3608
|
+
const editableRoots = ((_a = rootRef.current) == null ? void 0 : _a.querySelectorAll(
|
|
3609
|
+
'[data-testid^="page-body-"] [data-lexical-editor="true"]'
|
|
3610
|
+
)) ?? [];
|
|
3611
|
+
editableRoots.forEach((editableRoot) => {
|
|
3612
|
+
editableRoot.style.backgroundColor = globalSelectionActive ? GLOBAL_SELECTION_BACKGROUND : "";
|
|
3613
|
+
editableRoot.style.color = globalSelectionActive ? GLOBAL_SELECTION_FOREGROUND : "";
|
|
3614
|
+
editableRoot.style.caretColor = globalSelectionActive ? "transparent" : "";
|
|
3615
|
+
});
|
|
3616
|
+
}, [globalSelectionActive, document2.pages.length]);
|
|
3617
|
+
React.useEffect(() => {
|
|
3618
|
+
if (!captureHistoryShortcutsOnWindow) {
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
const handleWindowKeyDown = (event) => {
|
|
3622
|
+
const hasModifier = event.metaKey || event.ctrlKey;
|
|
3623
|
+
if (!hasModifier) {
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
handleHistoryShortcut(event);
|
|
3627
|
+
};
|
|
3628
|
+
window.addEventListener("keydown", handleWindowKeyDown, { capture: true });
|
|
3629
|
+
const handleWindowBeforeInput = (event) => {
|
|
3630
|
+
if (event.inputType === "historyUndo") {
|
|
3631
|
+
event.preventDefault();
|
|
3632
|
+
clearGlobalSelection();
|
|
3633
|
+
undo();
|
|
3634
|
+
} else if (event.inputType === "historyRedo") {
|
|
3635
|
+
event.preventDefault();
|
|
3636
|
+
clearGlobalSelection();
|
|
3637
|
+
redo();
|
|
3638
|
+
}
|
|
3639
|
+
};
|
|
3640
|
+
window.addEventListener("beforeinput", handleWindowBeforeInput, { capture: true });
|
|
3641
|
+
return () => {
|
|
3642
|
+
window.removeEventListener("keydown", handleWindowKeyDown, { capture: true });
|
|
3643
|
+
window.removeEventListener("beforeinput", handleWindowBeforeInput, { capture: true });
|
|
3644
|
+
};
|
|
3645
|
+
}, [captureHistoryShortcutsOnWindow, clearGlobalSelection, handleHistoryShortcut, redo, undo]);
|
|
3646
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3647
|
+
"div",
|
|
3648
|
+
{
|
|
3649
|
+
ref: rootRef,
|
|
3650
|
+
className: `lex4-editor flex flex-col h-full ${className ?? ""}`,
|
|
3651
|
+
"data-testid": "lex4-editor",
|
|
3652
|
+
"data-global-selection-active": globalSelectionActive ? "true" : "false",
|
|
3653
|
+
onKeyDownCapture: handleKeyDownCapture,
|
|
3654
|
+
onMouseDownCapture: handleMouseDownCapture,
|
|
3655
|
+
children: [
|
|
3656
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3657
|
+
"textarea",
|
|
3658
|
+
{
|
|
3659
|
+
ref: selectionBufferRef,
|
|
3660
|
+
"aria-hidden": "true",
|
|
3661
|
+
"data-testid": "global-selection-buffer",
|
|
3662
|
+
readOnly: true,
|
|
3663
|
+
tabIndex: -1,
|
|
3664
|
+
className: "pointer-events-none fixed -left-[9999px] top-0 h-0 w-0 opacity-0"
|
|
3665
|
+
}
|
|
3666
|
+
),
|
|
3667
|
+
/* @__PURE__ */ jsxRuntime.jsx(Toolbar, {}),
|
|
3668
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden bg-gray-200", children: [
|
|
3669
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1 overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsx(DocumentView, {}) }),
|
|
3670
|
+
/* @__PURE__ */ jsxRuntime.jsx(HistorySidebar, {})
|
|
3671
|
+
] })
|
|
3672
|
+
]
|
|
3673
|
+
}
|
|
3674
|
+
);
|
|
3675
|
+
};
|
|
3676
|
+
const Lex4Editor = ({
|
|
3677
|
+
captureHistoryShortcutsOnWindow = true,
|
|
3678
|
+
initialDocument,
|
|
3679
|
+
onDocumentChange,
|
|
3680
|
+
className
|
|
3681
|
+
}) => {
|
|
3682
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3683
|
+
DocumentProvider,
|
|
3684
|
+
{
|
|
3685
|
+
initialDocument,
|
|
3686
|
+
onDocumentChange,
|
|
3687
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3688
|
+
EditorChrome,
|
|
3689
|
+
{
|
|
3690
|
+
captureHistoryShortcutsOnWindow,
|
|
3691
|
+
className
|
|
3692
|
+
}
|
|
3693
|
+
)
|
|
3694
|
+
}
|
|
3695
|
+
);
|
|
3696
|
+
};
|
|
3697
|
+
function useOverflowDetection(bodyHeight, onOverflow, debounceMs = 100) {
|
|
3698
|
+
const timerRef = React.useRef(null);
|
|
3699
|
+
const observerRef = React.useRef(null);
|
|
3700
|
+
const check = React.useCallback(
|
|
3701
|
+
(el) => {
|
|
3702
|
+
if (!onOverflow) return;
|
|
3703
|
+
if (el.scrollHeight > bodyHeight + 2) {
|
|
3704
|
+
onOverflow();
|
|
3705
|
+
}
|
|
3706
|
+
},
|
|
3707
|
+
[bodyHeight, onOverflow]
|
|
3708
|
+
);
|
|
3709
|
+
const attachRef = React.useCallback(
|
|
3710
|
+
(el) => {
|
|
3711
|
+
if (observerRef.current) {
|
|
3712
|
+
observerRef.current.disconnect();
|
|
3713
|
+
observerRef.current = null;
|
|
3714
|
+
}
|
|
3715
|
+
if (!el || !onOverflow) return;
|
|
3716
|
+
observerRef.current = new ResizeObserver(() => {
|
|
3717
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3718
|
+
timerRef.current = setTimeout(() => check(el), debounceMs);
|
|
3719
|
+
});
|
|
3720
|
+
observerRef.current.observe(el);
|
|
3721
|
+
},
|
|
3722
|
+
[check, debounceMs, onOverflow]
|
|
3723
|
+
);
|
|
3724
|
+
React.useEffect(() => {
|
|
3725
|
+
return () => {
|
|
3726
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3727
|
+
if (observerRef.current) observerRef.current.disconnect();
|
|
3728
|
+
};
|
|
3729
|
+
}, []);
|
|
3730
|
+
return { attachRef };
|
|
3731
|
+
}
|
|
3732
|
+
function useHeaderFooter(maxHeight, onHeightChange) {
|
|
3733
|
+
const observerRef = React.useRef(null);
|
|
3734
|
+
const lastHeightRef = React.useRef(0);
|
|
3735
|
+
const attachRef = React.useCallback(
|
|
3736
|
+
(el) => {
|
|
3737
|
+
if (observerRef.current) {
|
|
3738
|
+
observerRef.current.disconnect();
|
|
3739
|
+
observerRef.current = null;
|
|
3740
|
+
}
|
|
3741
|
+
if (!el) return;
|
|
3742
|
+
const measure = () => {
|
|
3743
|
+
const height = Math.min(el.scrollHeight, maxHeight);
|
|
3744
|
+
if (height !== lastHeightRef.current) {
|
|
3745
|
+
lastHeightRef.current = height;
|
|
3746
|
+
onHeightChange(height);
|
|
3747
|
+
}
|
|
3748
|
+
};
|
|
3749
|
+
observerRef.current = new ResizeObserver(measure);
|
|
3750
|
+
observerRef.current.observe(el);
|
|
3751
|
+
measure();
|
|
3752
|
+
},
|
|
3753
|
+
[maxHeight, onHeightChange]
|
|
3754
|
+
);
|
|
3755
|
+
React.useEffect(() => {
|
|
3756
|
+
return () => {
|
|
3757
|
+
if (observerRef.current) observerRef.current.disconnect();
|
|
3758
|
+
};
|
|
3759
|
+
}, []);
|
|
3760
|
+
return { attachRef };
|
|
3761
|
+
}
|
|
3762
|
+
exports.A4_HEIGHT_MM = A4_HEIGHT_MM;
|
|
3763
|
+
exports.A4_HEIGHT_PX = A4_HEIGHT_PX;
|
|
3764
|
+
exports.A4_WIDTH_MM = A4_WIDTH_MM;
|
|
3765
|
+
exports.A4_WIDTH_PX = A4_WIDTH_PX;
|
|
3766
|
+
exports.Lex4Editor = Lex4Editor;
|
|
3767
|
+
exports.MAX_FOOTER_HEIGHT_PX = MAX_FOOTER_HEIGHT_PX;
|
|
3768
|
+
exports.MAX_HEADER_HEIGHT_PX = MAX_HEADER_HEIGHT_PX;
|
|
3769
|
+
exports.createEmptyDocument = createEmptyDocument;
|
|
3770
|
+
exports.createEmptyPage = createEmptyPage;
|
|
3771
|
+
exports.useHeaderFooter = useHeaderFooter;
|
|
3772
|
+
exports.useOverflowDetection = useOverflowDetection;
|
|
3773
|
+
exports.usePagination = usePagination;
|
|
3774
|
+
//# sourceMappingURL=lex4-editor.cjs.map
|