@stll/folio-react 0.6.0 → 0.8.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.
Files changed (30) hide show
  1. package/dist/{CommentsSidebar-_utmEkA9.js → CommentsSidebar-Byn5lyrt.js} +127 -130
  2. package/dist/{FindReplaceDialog-DLdBekSR.js → FindReplaceDialog-CqKK82nE.js} +23 -18
  3. package/dist/FootnotePropertiesDialog-CixRdIiT.js +822 -0
  4. package/dist/ImagePositionDialog-DNodkoI2.js +1065 -0
  5. package/dist/ImagePropertiesDialog-CLsDIUWb.js +613 -0
  6. package/dist/InsertSymbolDialog-DCFLmfzn.js +376 -0
  7. package/dist/PageSetupDialog-COkgeOQn.js +820 -0
  8. package/dist/TablePropertiesDialog-B_AGrPR8.js +434 -0
  9. package/dist/{TextContextMenu-CG0V37wi.js → TextContextMenu-Ci7-M4oU.js} +199 -79
  10. package/dist/compat/eigenpal.d.ts +1 -1
  11. package/dist/compat/eigenpal.js +89 -17
  12. package/dist/dialogChrome-BoGYPgeu.js +26 -0
  13. package/dist/{dialogs-CzvXYJyG.js → dialogs-Bj2PdycR.js} +608 -352
  14. package/dist/{dialogs-j7qyw17x.d.ts → dialogs-DK8OVUT_.d.ts} +13 -1
  15. package/dist/dialogs.d.ts +2 -2
  16. package/dist/dialogs.js +9 -8
  17. package/dist/editor.css +1 -1
  18. package/dist/folio-ui-BgDDRsUX.js +1013 -0
  19. package/dist/index.d.ts +5 -4
  20. package/dist/index.js +12 -11
  21. package/dist/{renderAsync-B7UJsZye.d.ts → renderAsync-BmxQE7iO.d.ts} +12 -3
  22. package/dist/{renderAsync-CwHTsxBN.js → renderAsync-BtWYvQMT.js} +5717 -3275
  23. package/dist/standalone.css +1 -1
  24. package/package.json +8 -6
  25. package/dist/FootnotePropertiesDialog-CUjq9c6H.js +0 -267
  26. package/dist/ImagePositionDialog-BMuO9rLT.js +0 -395
  27. package/dist/ImagePropertiesDialog-C8iEgdmN.js +0 -198
  28. package/dist/PageSetupDialog-BKsSMCGb.js +0 -312
  29. package/dist/TablePropertiesDialog-Dw8gvL78.js +0 -159
  30. package/dist/useFindReplace-BPBpMWS9.js +0 -842
@@ -1,842 +0,0 @@
1
- import { createContext, useCallback, useContext, useState } from "react";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- import { prefersReducedMotionBehavior } from "@stll/folio-core/paged-layout/scrollNavigation";
4
- import { Checkbox } from "@base-ui/react/checkbox";
5
- import { Popover } from "@base-ui/react/popover";
6
- import { Dialog } from "@base-ui/react/dialog";
7
- import { Input } from "@base-ui/react/input";
8
- import { Menu } from "@base-ui/react/menu";
9
- import { Select } from "@base-ui/react/select";
10
- //#region src/lib/utils.ts
11
- /** Merge class names, filtering out falsy values. */
12
- function cn(...inputs) {
13
- return inputs.filter(Boolean).join(" ");
14
- }
15
- //#endregion
16
- //#region src/ui/defaults/button.tsx
17
- /**
18
- * Built-in, dependency-free Button used when a consumer does not inject one.
19
- *
20
- * Renders a native, accessible `<button>` honoring the `FolioButtonProps`
21
- * subset. The minimal default styling lives in `editor.css`
22
- * (`.folio-default-button*` classes, keyed off folio's `--doc-*` variables with
23
- * safe fallbacks) rather than inline styles, so a caller's `className` can
24
- * override the defaults through the normal cascade. Consumers that want
25
- * polished chrome inject their own design-system Button via `DocxEditor`'s
26
- * `components` prop.
27
- */
28
- function DefaultButton({ variant = "default", size = "sm", className, children, ...props }) {
29
- return /* @__PURE__ */ jsx("button", {
30
- type: "button",
31
- className: cn("folio-default-button", `folio-default-button--${variant}`, `folio-default-button--${size}`, className),
32
- ...props,
33
- children
34
- });
35
- }
36
- //#endregion
37
- //#region src/ui/defaults/checkbox.tsx
38
- /**
39
- * Built-in, dependency-light Checkbox used when a consumer does not inject one.
40
- * Wraps `@base-ui/react`'s accessible Checkbox primitive with a minimal check
41
- * indicator; consumers inject their own design-system Checkbox for full polish.
42
- */
43
- function DefaultCheckbox({ className, ...props }) {
44
- return /* @__PURE__ */ jsx(Checkbox.Root, {
45
- className: cn("folio-default-checkbox", className),
46
- ...props,
47
- children: /* @__PURE__ */ jsx(Checkbox.Indicator, {
48
- className: "folio-default-checkbox-indicator",
49
- children: /* @__PURE__ */ jsx("svg", {
50
- "aria-hidden": "true",
51
- fill: "none",
52
- height: "14",
53
- stroke: "currentColor",
54
- strokeLinecap: "round",
55
- strokeLinejoin: "round",
56
- strokeWidth: "3",
57
- viewBox: "0 0 24 24",
58
- width: "14",
59
- xmlns: "http://www.w3.org/2000/svg",
60
- children: /* @__PURE__ */ jsx("path", { d: "M5.252 12.7 10.2 18.63 18.748 5.37" })
61
- })
62
- })
63
- });
64
- }
65
- //#endregion
66
- //#region src/ui/defaults/color-picker.tsx
67
- /**
68
- * Built-in, dependency-light ColorPicker used when a consumer does not inject
69
- * one. Wraps `@base-ui/react`'s Popover primitive (focus management,
70
- * portalling, collision-aware positioning) and renders the `presets` as a row
71
- * of swatch buttons plus a native `<input type="color">` for a custom color.
72
- * `onSelect` emits a preset value or a 6-char uppercase hex (no `#`), matching
73
- * the design-system ColorPicker contract. Consumers inject a polished picker
74
- * via `DocxEditor`'s `components` prop.
75
- */
76
- const swatchColor = (preset) => preset.color ?? `#${preset.value}`;
77
- const normalizeHex = (hex) => hex.replace(/^#/u, "").toUpperCase();
78
- const EMPTY_PRESETS = [];
79
- function DefaultColorPicker({ value, onSelect, onClear, presets = EMPTY_PRESETS, columns = 8, children }) {
80
- return /* @__PURE__ */ jsxs(Popover.Root, { children: [/* @__PURE__ */ jsx(Popover.Trigger, {
81
- nativeButton: false,
82
- render: /* @__PURE__ */ jsx("div", { className: "folio-default-color-picker-trigger" }),
83
- children
84
- }), /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
85
- align: "start",
86
- className: "folio-default-popover-positioner folio-root",
87
- side: "bottom",
88
- sideOffset: 4,
89
- children: /* @__PURE__ */ jsxs(Popover.Popup, {
90
- className: "folio-default-color-picker-popup",
91
- children: [
92
- onClear && /* @__PURE__ */ jsx(Popover.Close, {
93
- className: "folio-default-color-picker-clear",
94
- onClick: onClear,
95
- children: "No color"
96
- }),
97
- /* @__PURE__ */ jsx("div", {
98
- className: "folio-default-color-picker-swatches",
99
- style: { gridTemplateColumns: `repeat(${columns}, 1fr)` },
100
- children: presets.map((preset) => /* @__PURE__ */ jsx(Popover.Close, {
101
- "aria-label": preset.label,
102
- className: cn("folio-default-color-picker-swatch", value === preset.value && "folio-default-color-picker-swatch--selected"),
103
- onClick: () => onSelect?.(preset.value),
104
- style: { backgroundColor: swatchColor(preset) },
105
- title: preset.label
106
- }, preset.value))
107
- }),
108
- /* @__PURE__ */ jsxs("label", {
109
- className: "folio-default-color-picker-custom",
110
- children: ["Custom", /* @__PURE__ */ jsx("input", {
111
- "aria-label": "Custom color",
112
- onChange: (event) => onSelect?.(normalizeHex(event.target.value)),
113
- type: "color",
114
- value: value ? `#${normalizeHex(value)}` : "#000000"
115
- })]
116
- })
117
- ]
118
- })
119
- }) })] });
120
- }
121
- //#endregion
122
- //#region src/ui/defaults/date-picker-popover.tsx
123
- /**
124
- * Built-in, dependency-light DatePickerPopover used when a consumer does not
125
- * inject one. Renders a native `<input type="date">` whose value is an ISO
126
- * `yyyy-mm-dd` string; `onChange` emits that string (or `null` when cleared),
127
- * matching the design-system picker contract. Consumers inject a polished
128
- * calendar via `DocxEditor`'s `components` prop.
129
- */
130
- const toISODate = (value) => {
131
- if (value === null) return "";
132
- if (value instanceof Date) return `${value.getFullYear().toString().padStart(4, "0")}-${(value.getMonth() + 1).toString().padStart(2, "0")}-${value.getDate().toString().padStart(2, "0")}`;
133
- return value.length >= 10 ? value.slice(0, 10) : value;
134
- };
135
- function DefaultDatePickerPopover({ value, onChange, defaultOpen = false }) {
136
- return /* @__PURE__ */ jsx("input", {
137
- autoFocus: defaultOpen,
138
- className: "folio-default-date-input",
139
- onChange: (event) => onChange(event.target.value || null),
140
- type: "date",
141
- value: toISODate(value)
142
- });
143
- }
144
- //#endregion
145
- //#region src/ui/defaults/dialog.tsx
146
- /**
147
- * Built-in, dependency-light Dialog parts used when a consumer does not inject
148
- * its own. Each part wraps the matching `@base-ui/react` primitive — the same
149
- * accessible primitive the app design system wraps — with minimal styling.
150
- * Folio's chrome positions the popup itself via `className`, so the default
151
- * popup is a bare portalled surface (no viewport grid); consumers inject a
152
- * polished Dialog through `DocxEditor`'s `components` prop.
153
- */
154
- function DefaultDialogRoot(props) {
155
- return /* @__PURE__ */ jsx(Dialog.Root, { ...props });
156
- }
157
- function DefaultDialogPortal({ children, ...props }) {
158
- return /* @__PURE__ */ jsx(Dialog.Portal, {
159
- ...props,
160
- children: /* @__PURE__ */ jsx("div", {
161
- className: "folio-root",
162
- style: { display: "contents" },
163
- children
164
- })
165
- });
166
- }
167
- function DefaultDialogBackdrop({ className, ...props }) {
168
- return /* @__PURE__ */ jsx(Dialog.Backdrop, {
169
- className: cn("folio-default-dialog-backdrop", className),
170
- ...props
171
- });
172
- }
173
- function DefaultDialogPopup({ className, ...props }) {
174
- return /* @__PURE__ */ jsx(Dialog.Popup, {
175
- className: cn("folio-default-dialog-popup", className),
176
- ...props
177
- });
178
- }
179
- function DefaultDialogTitle({ className, ...props }) {
180
- return /* @__PURE__ */ jsx(Dialog.Title, {
181
- className: cn("folio-default-dialog-title", className),
182
- ...props
183
- });
184
- }
185
- function DefaultDialogClose({ className, ...props }) {
186
- return /* @__PURE__ */ jsx(Dialog.Close, {
187
- className: cn("folio-default-dialog-close", className),
188
- ...props
189
- });
190
- }
191
- const DefaultDialog = {
192
- Root: DefaultDialogRoot,
193
- Portal: DefaultDialogPortal,
194
- Backdrop: DefaultDialogBackdrop,
195
- Popup: DefaultDialogPopup,
196
- Title: DefaultDialogTitle,
197
- Close: DefaultDialogClose
198
- };
199
- //#endregion
200
- //#region src/ui/defaults/input.tsx
201
- /**
202
- * Built-in, dependency-light Input used when a consumer does not inject one.
203
- *
204
- * Wraps `@base-ui/react`'s Input primitive, honoring the `nativeInput` escape
205
- * hatch (render a plain `<input>` when the caller needs uncontrolled native
206
- * behavior) and folio's `size` shorthand. Styling is intentionally minimal;
207
- * consumers inject their own design-system Input for full polish.
208
- */
209
- function DefaultInput({ className, size = "default", nativeInput = false, ...props }) {
210
- const inputClassName = cn("folio-default-input", className);
211
- const numericSize = typeof size === "number" ? size : void 0;
212
- if (nativeInput) return /* @__PURE__ */ jsx("input", {
213
- className: inputClassName,
214
- "data-size": typeof size === "string" ? size : void 0,
215
- size: numericSize,
216
- ...props
217
- });
218
- return /* @__PURE__ */ jsx(Input, {
219
- className: inputClassName,
220
- "data-size": typeof size === "string" ? size : void 0,
221
- size: numericSize,
222
- ...props
223
- });
224
- }
225
- //#endregion
226
- //#region src/ui/defaults/menu.tsx
227
- /**
228
- * Built-in, dependency-light Menu parts used when a consumer does not inject
229
- * its own. Each part wraps the matching `@base-ui/react` primitive (real
230
- * keyboard navigation, portalling, collision-aware positioning) with minimal
231
- * styling. Consumers inject a polished Menu via `DocxEditor`'s `components`
232
- * prop.
233
- */
234
- function DefaultMenuRoot(props) {
235
- return /* @__PURE__ */ jsx(Menu.Root, { ...props });
236
- }
237
- function DefaultMenuTrigger({ nativeButton = true, ...props }) {
238
- return /* @__PURE__ */ jsx(Menu.Trigger, {
239
- nativeButton,
240
- ...props
241
- });
242
- }
243
- function DefaultMenuPopup({ className, children, align = "center", side = "bottom", sideOffset = 4, alignOffset, ...props }) {
244
- return /* @__PURE__ */ jsx(Menu.Portal, { children: /* @__PURE__ */ jsx(Menu.Positioner, {
245
- align,
246
- alignOffset,
247
- className: "folio-default-menu-positioner folio-root",
248
- side,
249
- sideOffset,
250
- children: /* @__PURE__ */ jsx(Menu.Popup, {
251
- className: cn("folio-default-menu-popup", className),
252
- ...props,
253
- children
254
- })
255
- }) });
256
- }
257
- function DefaultMenuItem({ className, ...props }) {
258
- return /* @__PURE__ */ jsx(Menu.Item, {
259
- className: cn("folio-default-menu-item", className),
260
- ...props
261
- });
262
- }
263
- function DefaultMenuCheckboxItem({ className, children, ...props }) {
264
- return /* @__PURE__ */ jsxs(Menu.CheckboxItem, {
265
- className: cn("folio-default-menu-checkbox-item", className),
266
- ...props,
267
- children: [/* @__PURE__ */ jsx(Menu.CheckboxItemIndicator, {
268
- className: "folio-default-menu-checkbox-indicator",
269
- children: /* @__PURE__ */ jsx("svg", {
270
- "aria-hidden": "true",
271
- fill: "none",
272
- height: "14",
273
- stroke: "currentColor",
274
- strokeLinecap: "round",
275
- strokeLinejoin: "round",
276
- strokeWidth: "3",
277
- viewBox: "0 0 24 24",
278
- width: "14",
279
- xmlns: "http://www.w3.org/2000/svg",
280
- children: /* @__PURE__ */ jsx("path", { d: "M5.252 12.7 10.2 18.63 18.748 5.37" })
281
- })
282
- }), /* @__PURE__ */ jsx("span", { children })]
283
- });
284
- }
285
- function DefaultMenuGroup(props) {
286
- return /* @__PURE__ */ jsx(Menu.Group, { ...props });
287
- }
288
- function DefaultMenuGroupLabel({ className, ...props }) {
289
- return /* @__PURE__ */ jsx(Menu.GroupLabel, {
290
- className: cn("folio-default-menu-group-label", className),
291
- ...props
292
- });
293
- }
294
- function DefaultMenuSeparator({ className, ...props }) {
295
- return /* @__PURE__ */ jsx(Menu.Separator, {
296
- className: cn("folio-default-menu-separator", className),
297
- ...props
298
- });
299
- }
300
- const DefaultMenu = {
301
- Root: DefaultMenuRoot,
302
- Trigger: DefaultMenuTrigger,
303
- Popup: DefaultMenuPopup,
304
- Item: DefaultMenuItem,
305
- CheckboxItem: DefaultMenuCheckboxItem,
306
- Group: DefaultMenuGroup,
307
- GroupLabel: DefaultMenuGroupLabel,
308
- Separator: DefaultMenuSeparator
309
- };
310
- //#endregion
311
- //#region src/ui/defaults/outline-rail.tsx
312
- /**
313
- * Built-in, dependency-light OutlineRail used when a consumer does not inject
314
- * one. Renders the outline as a plain semantic `<nav>` list of clickable
315
- * entries indented by level; clicking an entry calls `onJump` with the resolved
316
- * scroll container. Consumers inject the polished tick-rail + hover panel via
317
- * `DocxEditor`'s `components` prop.
318
- */
319
- function DefaultOutlineRail({ items, scrollContainerRef, onJump, activeId, topOffset = 0, panelWidth = 300, ariaLabel = "Outline" }) {
320
- if (items.length < 2) return null;
321
- return /* @__PURE__ */ jsx("nav", {
322
- "aria-label": ariaLabel,
323
- className: "folio-default-outline-rail",
324
- style: {
325
- top: topOffset,
326
- width: panelWidth
327
- },
328
- children: /* @__PURE__ */ jsx("ul", {
329
- className: "folio-default-outline-list",
330
- children: items.map((item) => {
331
- const isActive = item.id === activeId;
332
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
333
- "aria-current": isActive ? "true" : void 0,
334
- className: cn("folio-default-outline-item", isActive && "folio-default-outline-item--active"),
335
- onClick: () => {
336
- const container = scrollContainerRef.current;
337
- if (container) onJump(item.id, container);
338
- },
339
- style: { paddingInlineStart: `${item.level * 12 + 8}px` },
340
- title: item.label,
341
- type: "button",
342
- children: item.label
343
- }) }, item.id);
344
- })
345
- })
346
- });
347
- }
348
- //#endregion
349
- //#region src/ui/defaults/popover.tsx
350
- /**
351
- * Built-in, dependency-light Popover parts used when a consumer does not inject
352
- * its own. Each part wraps the matching `@base-ui/react` primitive (real focus
353
- * management, portalling, and collision-aware positioning) with minimal
354
- * styling. Consumers inject a polished Popover via `DocxEditor`'s `components`
355
- * prop.
356
- */
357
- function DefaultPopoverRoot(props) {
358
- return /* @__PURE__ */ jsx(Popover.Root, { ...props });
359
- }
360
- function DefaultPopoverTrigger(props) {
361
- return /* @__PURE__ */ jsx(Popover.Trigger, { ...props });
362
- }
363
- function DefaultPopoverPopup({ className, children, side = "bottom", align = "center", sideOffset = 4, alignOffset = 0, ...props }) {
364
- return /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
365
- align,
366
- alignOffset,
367
- className: "folio-default-popover-positioner folio-root",
368
- side,
369
- sideOffset,
370
- children: /* @__PURE__ */ jsx(Popover.Popup, {
371
- className: cn("folio-default-popover-popup", className),
372
- ...props,
373
- children
374
- })
375
- }) });
376
- }
377
- function DefaultPopoverClose(props) {
378
- return /* @__PURE__ */ jsx(Popover.Close, { ...props });
379
- }
380
- const DefaultPopover = {
381
- Root: DefaultPopoverRoot,
382
- Trigger: DefaultPopoverTrigger,
383
- Popup: DefaultPopoverPopup,
384
- Close: DefaultPopoverClose
385
- };
386
- //#endregion
387
- //#region src/ui/defaults/select.tsx
388
- /**
389
- * Built-in, dependency-light Select parts used when a consumer does not inject
390
- * its own. Each part wraps the matching `@base-ui/react` primitive (real
391
- * listbox semantics, keyboard navigation, portalled positioning) with minimal
392
- * styling. The default popup omits the scroll arrows and item-aligned
393
- * positioning of a fully styled design-system Select; consumers inject their
394
- * own Select via `DocxEditor`'s `components` prop for that polish.
395
- */
396
- function DefaultSelectRoot(props) {
397
- return /* @__PURE__ */ jsx(Select.Root, { ...props });
398
- }
399
- function DefaultSelectTrigger({ className, children, size, ...props }) {
400
- return /* @__PURE__ */ jsxs(Select.Trigger, {
401
- className: cn("folio-default-select-trigger", className),
402
- "data-size": size,
403
- ...props,
404
- children: [children, /* @__PURE__ */ jsx(Select.Icon, {
405
- className: "folio-default-select-icon",
406
- children: /* @__PURE__ */ jsx("svg", {
407
- "aria-hidden": "true",
408
- fill: "none",
409
- height: "16",
410
- stroke: "currentColor",
411
- strokeLinecap: "round",
412
- strokeLinejoin: "round",
413
- strokeWidth: "2",
414
- viewBox: "0 0 24 24",
415
- width: "16",
416
- xmlns: "http://www.w3.org/2000/svg",
417
- children: /* @__PURE__ */ jsx("path", { d: "m7 15 5 5 5-5M7 9l5-5 5 5" })
418
- })
419
- })]
420
- });
421
- }
422
- function DefaultSelectValue({ className, ...props }) {
423
- return /* @__PURE__ */ jsx(Select.Value, {
424
- className: cn("folio-default-select-value", className),
425
- ...props
426
- });
427
- }
428
- function DefaultSelectPopup({ className, children, ...props }) {
429
- return /* @__PURE__ */ jsx(Select.Portal, { children: /* @__PURE__ */ jsx(Select.Positioner, {
430
- align: "start",
431
- className: "folio-default-select-positioner folio-root",
432
- side: "bottom",
433
- sideOffset: 4,
434
- children: /* @__PURE__ */ jsx(Select.Popup, {
435
- className: cn("folio-default-select-popup", className),
436
- ...props,
437
- children: /* @__PURE__ */ jsx(Select.List, { children })
438
- })
439
- }) });
440
- }
441
- function DefaultSelectItem({ className, children, ...props }) {
442
- return /* @__PURE__ */ jsx(Select.Item, {
443
- className: cn("folio-default-select-item", className),
444
- ...props,
445
- children: /* @__PURE__ */ jsx(Select.ItemText, { children })
446
- });
447
- }
448
- //#endregion
449
- //#region src/ui/folio-ui.tsx
450
- const DEFAULT_COMPONENTS = {
451
- Button: DefaultButton,
452
- Dialog: DefaultDialog,
453
- Select: {
454
- Root: DefaultSelectRoot,
455
- Trigger: DefaultSelectTrigger,
456
- Value: DefaultSelectValue,
457
- Popup: DefaultSelectPopup,
458
- Item: DefaultSelectItem
459
- },
460
- Menu: DefaultMenu,
461
- Popover: DefaultPopover,
462
- Input: DefaultInput,
463
- Checkbox: DefaultCheckbox,
464
- ColorPicker: DefaultColorPicker,
465
- DatePickerPopover: DefaultDatePickerPopover,
466
- OutlineRail: DefaultOutlineRail
467
- };
468
- const FolioUIContext = createContext(DEFAULT_COMPONENTS);
469
- /**
470
- * Merge a consumer's partial overrides over the built-in defaults: every key is
471
- * present (defaults fill the gaps), and each provided override wins. Pure, so
472
- * the resolution contract is unit-tested independently of React.
473
- */
474
- function resolveFolioComponents(components) {
475
- return components ? {
476
- ...DEFAULT_COMPONENTS,
477
- ...components
478
- } : DEFAULT_COMPONENTS;
479
- }
480
- function FolioUIProvider({ components, children }) {
481
- const value = resolveFolioComponents(components);
482
- return /* @__PURE__ */ jsx(FolioUIContext.Provider, {
483
- value,
484
- children
485
- });
486
- }
487
- function useFolioUI() {
488
- return useContext(FolioUIContext);
489
- }
490
- //#endregion
491
- //#region src/components/dialogs/findReplaceUtils.ts
492
- /**
493
- * Find & Replace Utility Functions
494
- *
495
- * Pure utility functions for text search, pattern matching, and document search.
496
- * Extracted from FindReplaceDialog.tsx.
497
- */
498
- /**
499
- * Create default find options
500
- */
501
- function createDefaultFindOptions() {
502
- return {
503
- matchCase: false,
504
- matchWholeWord: false,
505
- useRegex: false
506
- };
507
- }
508
- /**
509
- * Find all matches of search text in content
510
- */
511
- function findAllMatches(content, searchText, options) {
512
- if (!content || !searchText) return [];
513
- const matches = [];
514
- let searchFor = searchText;
515
- if (!options.matchCase) searchFor = searchText.toLowerCase();
516
- const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
517
- let pattern;
518
- if (options.matchWholeWord) pattern = `\\b${escapeRegex(searchFor)}\\b`;
519
- else pattern = escapeRegex(searchFor);
520
- const flags = options.matchCase ? "g" : "gi";
521
- const regex = new RegExp(pattern, flags);
522
- let match;
523
- while ((match = regex.exec(content)) !== null) {
524
- matches.push({
525
- start: match.index,
526
- end: match.index + match[0].length
527
- });
528
- if (match[0].length === 0) regex.lastIndex++;
529
- }
530
- return matches;
531
- }
532
- /**
533
- * Get plain text from a run
534
- */
535
- function getRunText(run) {
536
- let text = "";
537
- for (const item of run.content) if (item.type === "text") text += item.text;
538
- else if (item.type === "tab") text += " ";
539
- else if (item.type === "break" && item.breakType === "textWrapping") text += "\n";
540
- return text;
541
- }
542
- function getHyperlinkText(hyperlink) {
543
- let text = "";
544
- for (const child of hyperlink.children) if (child.type === "run") text += getRunText(child);
545
- return text;
546
- }
547
- function getParagraphContentText(content) {
548
- if (content.type === "run") return getRunText(content);
549
- if (content.type === "hyperlink") return getHyperlinkText(content);
550
- if (content.type === "inlineSdt") {
551
- let text = "";
552
- for (const child of content.content) text += getParagraphContentText(child);
553
- return text;
554
- }
555
- if (content.type === "simpleField") {
556
- let text = "";
557
- for (const child of content.content) {
558
- if (child.type === "run") {
559
- text += getRunText(child);
560
- continue;
561
- }
562
- text += getHyperlinkText(child);
563
- }
564
- return text;
565
- }
566
- if (content.type === "complexField") {
567
- let text = "";
568
- for (const run of content.fieldResult) text += getRunText(run);
569
- return text;
570
- }
571
- return "";
572
- }
573
- /**
574
- * Get plain text from a paragraph
575
- */
576
- function getParagraphPlainText(paragraph) {
577
- let text = "";
578
- for (const item of paragraph.content) text += getParagraphContentText(item);
579
- return text;
580
- }
581
- /**
582
- * Find all matches in a document
583
- */
584
- function findInDocument(document, searchText, options) {
585
- if (!document || !searchText) return [];
586
- const matches = [];
587
- const body = document.package.document;
588
- if (!isDocumentBody(body)) return matches;
589
- forEachParagraph(body.content, (block, paragraphIndex) => {
590
- const paragraphMatches = findInParagraph(block, searchText, options, paragraphIndex);
591
- matches.push(...paragraphMatches);
592
- });
593
- return matches;
594
- }
595
- function forEachParagraph(blocks, visit) {
596
- let paragraphIndex = 0;
597
- const walkBlocks = (items) => {
598
- for (const block of items) {
599
- if (isParagraph(block)) {
600
- visit(block, paragraphIndex);
601
- paragraphIndex++;
602
- continue;
603
- }
604
- if (isTable(block)) {
605
- walkTable(block);
606
- continue;
607
- }
608
- if (isBlockSdt(block)) walkBlocks(block.content);
609
- }
610
- };
611
- const walkTable = (table) => {
612
- for (const row of table.rows) {
613
- if (!isTableRow(row)) continue;
614
- for (const cell of row.cells) {
615
- if (!isTableCell(cell)) continue;
616
- walkBlocks(cell.content);
617
- }
618
- }
619
- };
620
- walkBlocks(blocks);
621
- }
622
- function isRecord(value) {
623
- return typeof value === "object" && value !== null;
624
- }
625
- function isDocumentBody(value) {
626
- return isRecord(value) && Array.isArray(value["content"]);
627
- }
628
- function isParagraph(value) {
629
- return isRecord(value) && value["type"] === "paragraph" && Array.isArray(value["content"]);
630
- }
631
- function isTable(value) {
632
- return isRecord(value) && value["type"] === "table" && Array.isArray(value["rows"]);
633
- }
634
- function isTableRow(value) {
635
- return isRecord(value) && Array.isArray(value["cells"]);
636
- }
637
- function isTableCell(value) {
638
- return isRecord(value) && Array.isArray(value["content"]);
639
- }
640
- function isBlockSdt(value) {
641
- return isRecord(value) && value["type"] === "blockSdt" && Array.isArray(value["content"]);
642
- }
643
- /**
644
- * Find matches in a single paragraph
645
- */
646
- function findInParagraph(paragraph, searchText, options, paragraphIndex) {
647
- const matches = [];
648
- const paragraphText = getParagraphPlainText(paragraph);
649
- if (!paragraphText) return matches;
650
- const textMatches = findAllMatches(paragraphText, searchText, options);
651
- for (const match of textMatches) {
652
- const contentInfo = findContentAtOffset(paragraph, match.start);
653
- matches.push({
654
- paragraphIndex,
655
- contentIndex: contentInfo.contentIndex,
656
- startOffset: match.start,
657
- endOffset: match.end,
658
- text: paragraphText.slice(match.start, match.end)
659
- });
660
- }
661
- return matches;
662
- }
663
- /**
664
- * Find the content (run) at a specific character offset in a paragraph
665
- */
666
- function findContentAtOffset(paragraph, offset) {
667
- let currentOffset = 0;
668
- let contentIndex = 0;
669
- for (const item of paragraph.content) {
670
- const itemLength = getParagraphContentText(item).length;
671
- if (currentOffset + itemLength > offset) return {
672
- contentIndex,
673
- runIndex: contentIndex,
674
- offsetInContent: offset - currentOffset
675
- };
676
- currentOffset += itemLength;
677
- contentIndex++;
678
- }
679
- return {
680
- contentIndex: Math.max(0, paragraph.content.length - 1),
681
- runIndex: Math.max(0, paragraph.content.length - 1),
682
- offsetInContent: 0
683
- };
684
- }
685
- /**
686
- * Scroll to a match in the document
687
- */
688
- function scrollToMatch(containerElement, match) {
689
- if (!containerElement) return;
690
- (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({
691
- behavior: prefersReducedMotionBehavior(),
692
- block: "center"
693
- });
694
- }
695
- //#endregion
696
- //#region src/components/dialogs/useFindReplace.ts
697
- /**
698
- * useFindReplace Hook
699
- *
700
- * React hook for managing find/replace dialog state.
701
- * Extracted from FindReplaceDialog.tsx.
702
- */
703
- /**
704
- * Hook for managing find/replace dialog state
705
- */
706
- function useFindReplace(hookOptions) {
707
- const [state, setState] = useState({
708
- ...closedDialogState(hookOptions?.initialReplaceMode ? "replace" : "find"),
709
- searchText: "",
710
- replaceText: "",
711
- options: createDefaultFindOptions(),
712
- matches: [],
713
- currentIndex: 0
714
- });
715
- return {
716
- state,
717
- openFind: useCallback((selectedText) => {
718
- setState((prev) => ({
719
- ...prev,
720
- ...openDialogState("find"),
721
- searchText: selectedText || prev.searchText,
722
- matches: [],
723
- currentIndex: 0
724
- }));
725
- }, []),
726
- openReplace: useCallback((selectedText) => {
727
- setState((prev) => ({
728
- ...prev,
729
- ...openDialogState("replace"),
730
- searchText: selectedText || prev.searchText,
731
- matches: [],
732
- currentIndex: 0
733
- }));
734
- }, []),
735
- close: useCallback(() => {
736
- setState((prev) => ({
737
- ...prev,
738
- ...closedDialogState(prev.lastMode)
739
- }));
740
- }, []),
741
- toggle: useCallback(() => {
742
- setState((prev) => ({
743
- ...prev,
744
- ...prev.dialog.status === "closed" ? openDialogState(prev.lastMode) : closedDialogState(prev.lastMode)
745
- }));
746
- }, []),
747
- setSearchText: useCallback((text) => {
748
- setState((prev) => ({
749
- ...prev,
750
- searchText: text
751
- }));
752
- }, []),
753
- setReplaceText: useCallback((text) => {
754
- setState((prev) => ({
755
- ...prev,
756
- replaceText: text
757
- }));
758
- }, []),
759
- setOptions: useCallback((options) => {
760
- setState((prev) => ({
761
- ...prev,
762
- options: {
763
- ...prev.options,
764
- ...options
765
- }
766
- }));
767
- }, []),
768
- setMatches: useCallback((matches, currentIndex = 0) => {
769
- const newIndex = Math.max(0, Math.min(currentIndex, matches.length - 1));
770
- setState((prev) => ({
771
- ...prev,
772
- matches,
773
- currentIndex: matches.length > 0 ? newIndex : 0
774
- }));
775
- hookOptions?.onMatchesChange?.(matches);
776
- if (matches.length > 0) hookOptions?.onCurrentMatchChange?.(matches[newIndex] ?? null, newIndex);
777
- else hookOptions?.onCurrentMatchChange?.(null, -1);
778
- }, [hookOptions]),
779
- goToNextMatch: useCallback(() => {
780
- let newIndex = 0;
781
- setState((prev) => {
782
- if (prev.matches.length === 0) return prev;
783
- newIndex = (prev.currentIndex + 1) % prev.matches.length;
784
- return {
785
- ...prev,
786
- currentIndex: newIndex
787
- };
788
- });
789
- return newIndex;
790
- }, []),
791
- goToPreviousMatch: useCallback(() => {
792
- let newIndex = 0;
793
- setState((prev) => {
794
- if (prev.matches.length === 0) return prev;
795
- newIndex = prev.currentIndex === 0 ? prev.matches.length - 1 : prev.currentIndex - 1;
796
- return {
797
- ...prev,
798
- currentIndex: newIndex
799
- };
800
- });
801
- return newIndex;
802
- }, []),
803
- goToMatch: useCallback((index) => {
804
- setState((prev) => {
805
- if (prev.matches.length === 0 || index < 0 || index >= prev.matches.length) return prev;
806
- return {
807
- ...prev,
808
- currentIndex: index
809
- };
810
- });
811
- }, []),
812
- getCurrentMatch: useCallback(() => {
813
- if (state.matches.length === 0) return null;
814
- return state.matches[state.currentIndex] || null;
815
- }, [state.matches, state.currentIndex]),
816
- hasMatches: useCallback(() => state.matches.length > 0, [state.matches.length])
817
- };
818
- }
819
- function closedDialogState(mode) {
820
- return {
821
- dialog: { status: "closed" },
822
- lastMode: mode
823
- };
824
- }
825
- function openDialogState(mode) {
826
- if (mode === "replace") return {
827
- dialog: {
828
- status: "open",
829
- mode: "replace"
830
- },
831
- lastMode: "replace"
832
- };
833
- return {
834
- dialog: {
835
- status: "open",
836
- mode: "find"
837
- },
838
- lastMode: "find"
839
- };
840
- }
841
- //#endregion
842
- export { FolioUIProvider as a, DEFAULT_COMPONENTS as i, findInDocument as n, useFolioUI as o, scrollToMatch as r, cn as s, useFindReplace as t };