@stll/folio-react 0.3.0 → 0.5.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.
@@ -1,354 +0,0 @@
1
- import { useCallback, useState } from "react";
2
- import { prefersReducedMotionBehavior } from "@stll/folio-core/paged-layout/scrollNavigation";
3
- //#region src/components/dialogs/findReplaceUtils.ts
4
- /**
5
- * Find & Replace Utility Functions
6
- *
7
- * Pure utility functions for text search, pattern matching, and document search.
8
- * Extracted from FindReplaceDialog.tsx.
9
- */
10
- /**
11
- * Create default find options
12
- */
13
- function createDefaultFindOptions() {
14
- return {
15
- matchCase: false,
16
- matchWholeWord: false,
17
- useRegex: false
18
- };
19
- }
20
- /**
21
- * Find all matches of search text in content
22
- */
23
- function findAllMatches(content, searchText, options) {
24
- if (!content || !searchText) return [];
25
- const matches = [];
26
- let searchFor = searchText;
27
- if (!options.matchCase) searchFor = searchText.toLowerCase();
28
- const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
29
- let pattern;
30
- if (options.matchWholeWord) pattern = `\\b${escapeRegex(searchFor)}\\b`;
31
- else pattern = escapeRegex(searchFor);
32
- const flags = options.matchCase ? "g" : "gi";
33
- const regex = new RegExp(pattern, flags);
34
- let match;
35
- while ((match = regex.exec(content)) !== null) {
36
- matches.push({
37
- start: match.index,
38
- end: match.index + match[0].length
39
- });
40
- if (match[0].length === 0) regex.lastIndex++;
41
- }
42
- return matches;
43
- }
44
- /**
45
- * Get plain text from a run
46
- */
47
- function getRunText(run) {
48
- let text = "";
49
- for (const item of run.content) if (item.type === "text") text += item.text;
50
- else if (item.type === "tab") text += " ";
51
- else if (item.type === "break" && item.breakType === "textWrapping") text += "\n";
52
- return text;
53
- }
54
- function getHyperlinkText(hyperlink) {
55
- let text = "";
56
- for (const child of hyperlink.children) if (child.type === "run") text += getRunText(child);
57
- return text;
58
- }
59
- function getParagraphContentText(content) {
60
- if (content.type === "run") return getRunText(content);
61
- if (content.type === "hyperlink") return getHyperlinkText(content);
62
- if (content.type === "inlineSdt") {
63
- let text = "";
64
- for (const child of content.content) text += getParagraphContentText(child);
65
- return text;
66
- }
67
- if (content.type === "simpleField") {
68
- let text = "";
69
- for (const child of content.content) {
70
- if (child.type === "run") {
71
- text += getRunText(child);
72
- continue;
73
- }
74
- text += getHyperlinkText(child);
75
- }
76
- return text;
77
- }
78
- if (content.type === "complexField") {
79
- let text = "";
80
- for (const run of content.fieldResult) text += getRunText(run);
81
- return text;
82
- }
83
- return "";
84
- }
85
- /**
86
- * Get plain text from a paragraph
87
- */
88
- function getParagraphPlainText(paragraph) {
89
- let text = "";
90
- for (const item of paragraph.content) text += getParagraphContentText(item);
91
- return text;
92
- }
93
- /**
94
- * Find all matches in a document
95
- */
96
- function findInDocument(document, searchText, options) {
97
- if (!document || !searchText) return [];
98
- const matches = [];
99
- const body = document.package.document;
100
- if (!isDocumentBody(body)) return matches;
101
- forEachParagraph(body.content, (block, paragraphIndex) => {
102
- const paragraphMatches = findInParagraph(block, searchText, options, paragraphIndex);
103
- matches.push(...paragraphMatches);
104
- });
105
- return matches;
106
- }
107
- function forEachParagraph(blocks, visit) {
108
- let paragraphIndex = 0;
109
- const walkBlocks = (items) => {
110
- for (const block of items) {
111
- if (isParagraph(block)) {
112
- visit(block, paragraphIndex);
113
- paragraphIndex++;
114
- continue;
115
- }
116
- if (isTable(block)) {
117
- walkTable(block);
118
- continue;
119
- }
120
- if (isBlockSdt(block)) walkBlocks(block.content);
121
- }
122
- };
123
- const walkTable = (table) => {
124
- for (const row of table.rows) {
125
- if (!isTableRow(row)) continue;
126
- for (const cell of row.cells) {
127
- if (!isTableCell(cell)) continue;
128
- walkBlocks(cell.content);
129
- }
130
- }
131
- };
132
- walkBlocks(blocks);
133
- }
134
- function isRecord(value) {
135
- return typeof value === "object" && value !== null;
136
- }
137
- function isDocumentBody(value) {
138
- return isRecord(value) && Array.isArray(value["content"]);
139
- }
140
- function isParagraph(value) {
141
- return isRecord(value) && value["type"] === "paragraph" && Array.isArray(value["content"]);
142
- }
143
- function isTable(value) {
144
- return isRecord(value) && value["type"] === "table" && Array.isArray(value["rows"]);
145
- }
146
- function isTableRow(value) {
147
- return isRecord(value) && Array.isArray(value["cells"]);
148
- }
149
- function isTableCell(value) {
150
- return isRecord(value) && Array.isArray(value["content"]);
151
- }
152
- function isBlockSdt(value) {
153
- return isRecord(value) && value["type"] === "blockSdt" && Array.isArray(value["content"]);
154
- }
155
- /**
156
- * Find matches in a single paragraph
157
- */
158
- function findInParagraph(paragraph, searchText, options, paragraphIndex) {
159
- const matches = [];
160
- const paragraphText = getParagraphPlainText(paragraph);
161
- if (!paragraphText) return matches;
162
- const textMatches = findAllMatches(paragraphText, searchText, options);
163
- for (const match of textMatches) {
164
- const contentInfo = findContentAtOffset(paragraph, match.start);
165
- matches.push({
166
- paragraphIndex,
167
- contentIndex: contentInfo.contentIndex,
168
- startOffset: match.start,
169
- endOffset: match.end,
170
- text: paragraphText.slice(match.start, match.end)
171
- });
172
- }
173
- return matches;
174
- }
175
- /**
176
- * Find the content (run) at a specific character offset in a paragraph
177
- */
178
- function findContentAtOffset(paragraph, offset) {
179
- let currentOffset = 0;
180
- let contentIndex = 0;
181
- for (const item of paragraph.content) {
182
- const itemLength = getParagraphContentText(item).length;
183
- if (currentOffset + itemLength > offset) return {
184
- contentIndex,
185
- runIndex: contentIndex,
186
- offsetInContent: offset - currentOffset
187
- };
188
- currentOffset += itemLength;
189
- contentIndex++;
190
- }
191
- return {
192
- contentIndex: Math.max(0, paragraph.content.length - 1),
193
- runIndex: Math.max(0, paragraph.content.length - 1),
194
- offsetInContent: 0
195
- };
196
- }
197
- /**
198
- * Scroll to a match in the document
199
- */
200
- function scrollToMatch(containerElement, match) {
201
- if (!containerElement) return;
202
- (containerElement.querySelector(`[data-paragraph-index="${match.paragraphIndex}"]`) ?? containerElement.querySelector(`.layout-paragraph[data-block-id="block-${match.paragraphIndex + 1}"]`) ?? containerElement.querySelectorAll(".layout-paragraph").item(match.paragraphIndex)).scrollIntoView({
203
- behavior: prefersReducedMotionBehavior(),
204
- block: "center"
205
- });
206
- }
207
- //#endregion
208
- //#region src/components/dialogs/useFindReplace.ts
209
- /**
210
- * useFindReplace Hook
211
- *
212
- * React hook for managing find/replace dialog state.
213
- * Extracted from FindReplaceDialog.tsx.
214
- */
215
- /**
216
- * Hook for managing find/replace dialog state
217
- */
218
- function useFindReplace(hookOptions) {
219
- const [state, setState] = useState({
220
- ...closedDialogState(hookOptions?.initialReplaceMode ? "replace" : "find"),
221
- searchText: "",
222
- replaceText: "",
223
- options: createDefaultFindOptions(),
224
- matches: [],
225
- currentIndex: 0
226
- });
227
- return {
228
- state,
229
- openFind: useCallback((selectedText) => {
230
- setState((prev) => ({
231
- ...prev,
232
- ...openDialogState("find"),
233
- searchText: selectedText || prev.searchText,
234
- matches: [],
235
- currentIndex: 0
236
- }));
237
- }, []),
238
- openReplace: useCallback((selectedText) => {
239
- setState((prev) => ({
240
- ...prev,
241
- ...openDialogState("replace"),
242
- searchText: selectedText || prev.searchText,
243
- matches: [],
244
- currentIndex: 0
245
- }));
246
- }, []),
247
- close: useCallback(() => {
248
- setState((prev) => ({
249
- ...prev,
250
- ...closedDialogState(prev.lastMode)
251
- }));
252
- }, []),
253
- toggle: useCallback(() => {
254
- setState((prev) => ({
255
- ...prev,
256
- ...prev.dialog.status === "closed" ? openDialogState(prev.lastMode) : closedDialogState(prev.lastMode)
257
- }));
258
- }, []),
259
- setSearchText: useCallback((text) => {
260
- setState((prev) => ({
261
- ...prev,
262
- searchText: text
263
- }));
264
- }, []),
265
- setReplaceText: useCallback((text) => {
266
- setState((prev) => ({
267
- ...prev,
268
- replaceText: text
269
- }));
270
- }, []),
271
- setOptions: useCallback((options) => {
272
- setState((prev) => ({
273
- ...prev,
274
- options: {
275
- ...prev.options,
276
- ...options
277
- }
278
- }));
279
- }, []),
280
- setMatches: useCallback((matches, currentIndex = 0) => {
281
- const newIndex = Math.max(0, Math.min(currentIndex, matches.length - 1));
282
- setState((prev) => ({
283
- ...prev,
284
- matches,
285
- currentIndex: matches.length > 0 ? newIndex : 0
286
- }));
287
- hookOptions?.onMatchesChange?.(matches);
288
- if (matches.length > 0) hookOptions?.onCurrentMatchChange?.(matches[newIndex] ?? null, newIndex);
289
- else hookOptions?.onCurrentMatchChange?.(null, -1);
290
- }, [hookOptions]),
291
- goToNextMatch: useCallback(() => {
292
- let newIndex = 0;
293
- setState((prev) => {
294
- if (prev.matches.length === 0) return prev;
295
- newIndex = (prev.currentIndex + 1) % prev.matches.length;
296
- return {
297
- ...prev,
298
- currentIndex: newIndex
299
- };
300
- });
301
- return newIndex;
302
- }, []),
303
- goToPreviousMatch: useCallback(() => {
304
- let newIndex = 0;
305
- setState((prev) => {
306
- if (prev.matches.length === 0) return prev;
307
- newIndex = prev.currentIndex === 0 ? prev.matches.length - 1 : prev.currentIndex - 1;
308
- return {
309
- ...prev,
310
- currentIndex: newIndex
311
- };
312
- });
313
- return newIndex;
314
- }, []),
315
- goToMatch: useCallback((index) => {
316
- setState((prev) => {
317
- if (prev.matches.length === 0 || index < 0 || index >= prev.matches.length) return prev;
318
- return {
319
- ...prev,
320
- currentIndex: index
321
- };
322
- });
323
- }, []),
324
- getCurrentMatch: useCallback(() => {
325
- if (state.matches.length === 0) return null;
326
- return state.matches[state.currentIndex] || null;
327
- }, [state.matches, state.currentIndex]),
328
- hasMatches: useCallback(() => state.matches.length > 0, [state.matches.length])
329
- };
330
- }
331
- function closedDialogState(mode) {
332
- return {
333
- dialog: { status: "closed" },
334
- lastMode: mode
335
- };
336
- }
337
- function openDialogState(mode) {
338
- if (mode === "replace") return {
339
- dialog: {
340
- status: "open",
341
- mode: "replace"
342
- },
343
- lastMode: "replace"
344
- };
345
- return {
346
- dialog: {
347
- status: "open",
348
- mode: "find"
349
- },
350
- lastMode: "find"
351
- };
352
- }
353
- //#endregion
354
- export { findInParagraph as a, findInDocument as i, createDefaultFindOptions as n, scrollToMatch as o, findAllMatches as r, useFindReplace as t };