@terra-symposium/docs 0.0.0-stage → 0.1.17
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 +201 -0
- package/README.md +72 -2
- package/lib-dist/Docs.d.ts +22 -0
- package/lib-dist/Docs.d.ts.map +1 -0
- package/lib-dist/Docs.js +60 -0
- package/lib-dist/Docs.js.map +1 -0
- package/lib-dist/LocalDocsWorkspace.d.ts +30 -0
- package/lib-dist/LocalDocsWorkspace.d.ts.map +1 -0
- package/lib-dist/LocalDocsWorkspace.js +148 -0
- package/lib-dist/LocalDocsWorkspace.js.map +1 -0
- package/lib-dist/SiteLayout.d.ts +30 -0
- package/lib-dist/SiteLayout.d.ts.map +1 -0
- package/lib-dist/SiteLayout.js +34 -0
- package/lib-dist/SiteLayout.js.map +1 -0
- package/lib-dist/StaticWorkspace.d.ts +21 -0
- package/lib-dist/StaticWorkspace.d.ts.map +1 -0
- package/lib-dist/StaticWorkspace.js +179 -0
- package/lib-dist/StaticWorkspace.js.map +1 -0
- package/lib-dist/_Toolbar.d.ts +7 -0
- package/lib-dist/_Toolbar.d.ts.map +1 -0
- package/lib-dist/_Toolbar.js +85 -0
- package/lib-dist/_Toolbar.js.map +1 -0
- package/lib-dist/blocks.d.ts +23 -0
- package/lib-dist/blocks.d.ts.map +1 -0
- package/lib-dist/blocks.js +321 -0
- package/lib-dist/blocks.js.map +1 -0
- package/lib-dist/docs-catalog.d.ts +5 -0
- package/lib-dist/docs-catalog.d.ts.map +1 -0
- package/lib-dist/docs-catalog.js +38 -0
- package/lib-dist/docs-catalog.js.map +1 -0
- package/lib-dist/docs-style.css +31 -0
- package/lib-dist/fonts/Avara-Bold.woff2 +0 -0
- package/lib-dist/fonts/Avara-BoldItalic.woff2 +0 -0
- package/lib-dist/fonts/OFL-1.1.txt +93 -0
- package/lib-dist/fonts/README.md +5 -0
- package/lib-dist/host-runtime.d.ts +5 -0
- package/lib-dist/host-runtime.d.ts.map +1 -0
- package/lib-dist/host-runtime.js +21 -0
- package/lib-dist/host-runtime.js.map +1 -0
- package/lib-dist/host.d.ts +170 -0
- package/lib-dist/host.d.ts.map +1 -0
- package/lib-dist/host.js +2 -0
- package/lib-dist/host.js.map +1 -0
- package/lib-dist/index.d.ts +35 -0
- package/lib-dist/index.d.ts.map +1 -0
- package/lib-dist/index.js +28 -0
- package/lib-dist/index.js.map +1 -0
- package/lib-dist/markdown-model.d.ts +7 -0
- package/lib-dist/markdown-model.d.ts.map +1 -0
- package/lib-dist/markdown-model.js +181 -0
- package/lib-dist/markdown-model.js.map +1 -0
- package/lib-dist/markdown.d.ts +3 -0
- package/lib-dist/markdown.d.ts.map +1 -0
- package/lib-dist/markdown.js +83 -0
- package/lib-dist/markdown.js.map +1 -0
- package/lib-dist/navigation.d.ts +17 -0
- package/lib-dist/navigation.d.ts.map +1 -0
- package/lib-dist/navigation.js +9 -0
- package/lib-dist/navigation.js.map +1 -0
- package/lib-dist/schema.d.ts +135 -0
- package/lib-dist/schema.d.ts.map +1 -0
- package/lib-dist/schema.js +55 -0
- package/lib-dist/schema.js.map +1 -0
- package/lib-dist/style.css +1 -0
- package/lib-dist/terra/Workspace.d.ts +5 -0
- package/lib-dist/terra/Workspace.d.ts.map +1 -0
- package/lib-dist/terra/Workspace.js +9 -0
- package/lib-dist/terra/Workspace.js.map +1 -0
- package/lib-dist/terra/editor/TerraContentEditor.d.ts +10 -0
- package/lib-dist/terra/editor/TerraContentEditor.d.ts.map +1 -0
- package/lib-dist/terra/editor/TerraContentEditor.js +47 -0
- package/lib-dist/terra/editor/TerraContentEditor.js.map +1 -0
- package/lib-dist/terra/editor/_blocks.d.ts +71 -0
- package/lib-dist/terra/editor/_blocks.d.ts.map +1 -0
- package/lib-dist/terra/editor/_blocks.js +110 -0
- package/lib-dist/terra/editor/_blocks.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Callout.d.ts +14 -0
- package/lib-dist/terra/editor/docs/_Callout.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Callout.js +41 -0
- package/lib-dist/terra/editor/docs/_Callout.js.map +1 -0
- package/lib-dist/terra/editor/docs/_CodeBlock.d.ts +39 -0
- package/lib-dist/terra/editor/docs/_CodeBlock.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_CodeBlock.js +494 -0
- package/lib-dist/terra/editor/docs/_CodeBlock.js.map +1 -0
- package/lib-dist/terra/editor/docs/_DocsCode.d.ts +14 -0
- package/lib-dist/terra/editor/docs/_DocsCode.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_DocsCode.js +95 -0
- package/lib-dist/terra/editor/docs/_DocsCode.js.map +1 -0
- package/lib-dist/terra/editor/docs/_DocsImage.d.ts +18 -0
- package/lib-dist/terra/editor/docs/_DocsImage.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_DocsImage.js +62 -0
- package/lib-dist/terra/editor/docs/_DocsImage.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Embed.d.ts +15 -0
- package/lib-dist/terra/editor/docs/_Embed.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Embed.js +27 -0
- package/lib-dist/terra/editor/docs/_Embed.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Math.d.ts +31 -0
- package/lib-dist/terra/editor/docs/_Math.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Math.js +126 -0
- package/lib-dist/terra/editor/docs/_Math.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Nodes.d.ts +15 -0
- package/lib-dist/terra/editor/docs/_Nodes.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Nodes.js +52 -0
- package/lib-dist/terra/editor/docs/_Nodes.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Raw.d.ts +15 -0
- package/lib-dist/terra/editor/docs/_Raw.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Raw.js +26 -0
- package/lib-dist/terra/editor/docs/_Raw.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Table.d.ts +12 -0
- package/lib-dist/terra/editor/docs/_Table.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Table.js +71 -0
- package/lib-dist/terra/editor/docs/_Table.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Tabs.d.ts +14 -0
- package/lib-dist/terra/editor/docs/_Tabs.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Tabs.js +104 -0
- package/lib-dist/terra/editor/docs/_Tabs.js.map +1 -0
- package/lib-dist/terra/editor/docs/_TsDocs.d.ts +15 -0
- package/lib-dist/terra/editor/docs/_TsDocs.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_TsDocs.js +81 -0
- package/lib-dist/terra/editor/docs/_TsDocs.js.map +1 -0
- package/lib-dist/terra/editor/docs/_math_inline.d.ts +14 -0
- package/lib-dist/terra/editor/docs/_math_inline.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_math_inline.js +30 -0
- package/lib-dist/terra/editor/docs/_math_inline.js.map +1 -0
- package/lib-dist/terra/editor/docs/heading.d.ts +23 -0
- package/lib-dist/terra/editor/docs/heading.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/heading.js +60 -0
- package/lib-dist/terra/editor/docs/heading.js.map +1 -0
- package/lib-dist/terra/editor/docs/index.d.ts +13 -0
- package/lib-dist/terra/editor/docs/index.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/index.js +14 -0
- package/lib-dist/terra/editor/docs/index.js.map +1 -0
- package/lib-dist/terra/editor/types.d.ts +40 -0
- package/lib-dist/terra/editor/types.d.ts.map +1 -0
- package/lib-dist/terra/editor/types.js +11 -0
- package/lib-dist/terra/editor/types.js.map +1 -0
- package/lib-dist/terra/flow/Nodes.d.ts +78 -0
- package/lib-dist/terra/flow/Nodes.d.ts.map +1 -0
- package/lib-dist/terra/flow/Nodes.js +912 -0
- package/lib-dist/terra/flow/Nodes.js.map +1 -0
- package/lib-dist/terra/flow/_chrome.d.ts +267 -0
- package/lib-dist/terra/flow/_chrome.d.ts.map +1 -0
- package/lib-dist/terra/flow/_chrome.js +798 -0
- package/lib-dist/terra/flow/_chrome.js.map +1 -0
- package/lib-dist/terra/flow/_layout.d.ts +30 -0
- package/lib-dist/terra/flow/_layout.d.ts.map +1 -0
- package/lib-dist/terra/flow/_layout.js +53 -0
- package/lib-dist/terra/flow/_layout.js.map +1 -0
- package/lib-dist/terra/flow/menuContext.d.ts +13 -0
- package/lib-dist/terra/flow/menuContext.d.ts.map +1 -0
- package/lib-dist/terra/flow/menuContext.js +10 -0
- package/lib-dist/terra/flow/menuContext.js.map +1 -0
- package/lib-dist/terra/flow/powerSource.d.ts +16 -0
- package/lib-dist/terra/flow/powerSource.d.ts.map +1 -0
- package/lib-dist/terra/flow/powerSource.js +59 -0
- package/lib-dist/terra/flow/powerSource.js.map +1 -0
- package/lib-dist/terra/pages/docs/_attachments.d.ts +20 -0
- package/lib-dist/terra/pages/docs/_attachments.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_attachments.js +904 -0
- package/lib-dist/terra/pages/docs/_attachments.js.map +1 -0
- package/lib-dist/terra/pages/docs/_blocks.d.ts +48 -0
- package/lib-dist/terra/pages/docs/_blocks.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_blocks.js +192 -0
- package/lib-dist/terra/pages/docs/_blocks.js.map +1 -0
- package/lib-dist/terra/pages/docs/_custom.d.ts +5 -0
- package/lib-dist/terra/pages/docs/_custom.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_custom.js +6 -0
- package/lib-dist/terra/pages/docs/_custom.js.map +1 -0
- package/lib-dist/terra/pages/docs/_editable.d.ts +61 -0
- package/lib-dist/terra/pages/docs/_editable.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_editable.js +404 -0
- package/lib-dist/terra/pages/docs/_editable.js.map +1 -0
- package/lib-dist/terra/pages/docs/_export-dialog.d.ts +19 -0
- package/lib-dist/terra/pages/docs/_export-dialog.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_export-dialog.js +78 -0
- package/lib-dist/terra/pages/docs/_export-dialog.js.map +1 -0
- package/lib-dist/terra/pages/docs/_helpers.d.ts +395 -0
- package/lib-dist/terra/pages/docs/_helpers.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_helpers.js +911 -0
- package/lib-dist/terra/pages/docs/_helpers.js.map +1 -0
- package/lib-dist/terra/pages/docs/_icons.d.ts +20 -0
- package/lib-dist/terra/pages/docs/_icons.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_icons.js +29 -0
- package/lib-dist/terra/pages/docs/_icons.js.map +1 -0
- package/lib-dist/terra/pages/docs/_main.d.ts +51 -0
- package/lib-dist/terra/pages/docs/_main.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_main.js +1959 -0
- package/lib-dist/terra/pages/docs/_main.js.map +1 -0
- package/lib-dist/terra/pages/docs/_sidebar.d.ts +42 -0
- package/lib-dist/terra/pages/docs/_sidebar.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_sidebar.js +2450 -0
- package/lib-dist/terra/pages/docs/_sidebar.js.map +1 -0
- package/lib-dist/terra/pages/docs/_toolbar.d.ts +23 -0
- package/lib-dist/terra/pages/docs/_toolbar.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_toolbar.js +1539 -0
- package/lib-dist/terra/pages/docs/_toolbar.js.map +1 -0
- package/lib-dist/terra/pages/docs/_tree.d.ts +178 -0
- package/lib-dist/terra/pages/docs/_tree.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_tree.js +1168 -0
- package/lib-dist/terra/pages/docs/_tree.js.map +1 -0
- package/lib-dist/terra/regex.mjs +398 -0
- package/lib-dist/terra/runtime/action-proxy.d.ts +5 -0
- package/lib-dist/terra/runtime/action-proxy.d.ts.map +1 -0
- package/lib-dist/terra/runtime/action-proxy.js +19 -0
- package/lib-dist/terra/runtime/action-proxy.js.map +1 -0
- package/lib-dist/terra/runtime/content.d.ts +150 -0
- package/lib-dist/terra/runtime/content.d.ts.map +1 -0
- package/lib-dist/terra/runtime/content.js +1171 -0
- package/lib-dist/terra/runtime/content.js.map +1 -0
- package/lib-dist/terra/runtime/leaf.d.ts +5 -0
- package/lib-dist/terra/runtime/leaf.d.ts.map +1 -0
- package/lib-dist/terra/runtime/leaf.js +13 -0
- package/lib-dist/terra/runtime/leaf.js.map +1 -0
- package/lib-dist/terra/runtime/nodes.d.ts +112 -0
- package/lib-dist/terra/runtime/nodes.d.ts.map +1 -0
- package/lib-dist/terra/runtime/nodes.js +534 -0
- package/lib-dist/terra/runtime/nodes.js.map +1 -0
- package/lib-dist/terra/runtime/runtime.d.ts +25 -0
- package/lib-dist/terra/runtime/runtime.d.ts.map +1 -0
- package/lib-dist/terra/runtime/runtime.js +50 -0
- package/lib-dist/terra/runtime/runtime.js.map +1 -0
- package/lib-dist/terra/runtime/shared.d.ts +35 -0
- package/lib-dist/terra/runtime/shared.d.ts.map +1 -0
- package/lib-dist/terra/runtime/shared.js +79 -0
- package/lib-dist/terra/runtime/shared.js.map +1 -0
- package/lib-dist/terra/runtime/types.d.ts +266 -0
- package/lib-dist/terra/runtime/types.d.ts.map +1 -0
- package/lib-dist/terra/runtime/types.js +18 -0
- package/lib-dist/terra/runtime/types.js.map +1 -0
- package/lib-dist/terra/runtime/ui.d.ts +31 -0
- package/lib-dist/terra/runtime/ui.d.ts.map +1 -0
- package/lib-dist/terra/runtime/ui.js +16 -0
- package/lib-dist/terra/runtime/ui.js.map +1 -0
- package/lib-dist/workspace.d.ts +78 -0
- package/lib-dist/workspace.d.ts.map +1 -0
- package/lib-dist/workspace.js +151 -0
- package/lib-dist/workspace.js.map +1 -0
- package/package.json +90 -3
- package/src/SiteLayout.astro +38 -0
- package/src/navigation.tsx +47 -0
- package/src/style.css +733 -0
|
@@ -0,0 +1,1959 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck -- source-preserving extraction; public host contracts are typed in src/host.ts.
|
|
3
|
+
/**
|
|
4
|
+
* astro/src/pages/docs/_main.tsx
|
|
5
|
+
* Docs scaffold entrypoint, shell, transition ctx, and compiled view/edit surfaces.
|
|
6
|
+
* Folder boundary: page composition and shared Docs state; block rendering, editing, chrome, and navigation remain separate.
|
|
7
|
+
*/
|
|
8
|
+
/*//////////////////////////////////////////////////
|
|
9
|
+
IMPORTS
|
|
10
|
+
//////////////////////////////////////////////////*/
|
|
11
|
+
// API
|
|
12
|
+
import { $cursor, $cursorGhost, $cursorPosition, $cursorTrack, $editReadyPath, $editorMounts, $leafTitle, $mode, $sidebar, $surfaceReady, $toolbar, captureLinearOffset, captureNodePath, codeViewOf, document, getCollapsedRange, htmlToMd, isDocBody, keepDeepPath, pasteMd, resetEditorMounts, setEditorMount, useAtom, useTransitionState, } from './_helpers.js';
|
|
13
|
+
import { components as customComponents } from './_custom.js';
|
|
14
|
+
import { cn, notify, setDocumentTitle } from '../../runtime/runtime.js';
|
|
15
|
+
import { currLeafSrc, toPortableBody } from '../../runtime/leaf.js';
|
|
16
|
+
import { dirname, IMAGE_TYPES } from '../../runtime/shared.js';
|
|
17
|
+
import { lazy, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
18
|
+
import { mdTableToHtml } from '../../runtime/content.js';
|
|
19
|
+
import { toImgAlt, toImgMd } from './_blocks.js';
|
|
20
|
+
import { toast } from 'sonner';
|
|
21
|
+
import { getDocsHostAdapter } from '../../../host-runtime.js';
|
|
22
|
+
import { docsRootPath } from '../../runtime/shared.js';
|
|
23
|
+
// COMPONENTS
|
|
24
|
+
import * as TerraQualityRegex from '../../regex.mjs';
|
|
25
|
+
import EditProvider, { Loader, TransitionShell } from './_editable.js';
|
|
26
|
+
import Nav from './_sidebar.js';
|
|
27
|
+
import React, { Suspense } from 'react';
|
|
28
|
+
import { AlertError } from '@terra-symposium/styling/ui/terra-original';
|
|
29
|
+
import { Attachments } from './_attachments.js';
|
|
30
|
+
import { Badge } from '@terra-symposium/styling/ui/terra-original';
|
|
31
|
+
import { Button } from '@terra-symposium/styling/ui/terra-original';
|
|
32
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@terra-symposium/styling/ui/terra-original';
|
|
33
|
+
import { Toolbar } from './_toolbar.js';
|
|
34
|
+
import { EditStateCtx, EditorSurfaceCtx, TransitionCtx } from './_helpers.js';
|
|
35
|
+
import { ErrorBoundary } from '../../runtime/ui.js';
|
|
36
|
+
import { Input } from '@terra-symposium/styling/ui/terra-original';
|
|
37
|
+
import { Label } from '@terra-symposium/styling/ui/terra-original';
|
|
38
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@terra-symposium/styling/ui/terra-original';
|
|
39
|
+
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@terra-symposium/styling/ui/terra-original';
|
|
40
|
+
import { Separator } from '@terra-symposium/styling/ui/terra-original';
|
|
41
|
+
import { Textarea } from '@terra-symposium/styling/ui/terra-original';
|
|
42
|
+
import { Toaster } from 'sonner';
|
|
43
|
+
// DATA
|
|
44
|
+
import { MIME } from '../../runtime/shared.js';
|
|
45
|
+
const TerraContentEditor = lazy(() => import('../../editor/TerraContentEditor.js').then(module => ({ default: module.TerraContentEditor })));
|
|
46
|
+
/*//////////////////////////////////////////////////
|
|
47
|
+
HOOKS
|
|
48
|
+
//////////////////////////////////////////////////*/
|
|
49
|
+
/**
|
|
50
|
+
* Handles DOM keyboard and pointer tracking for custom block els.
|
|
51
|
+
*
|
|
52
|
+
* @param refs - Refs input supplied to use block nav's operation.
|
|
53
|
+
*
|
|
54
|
+
* @return Selection, keyboard, and pointer handlers used to navigate and position custom editor blocks.
|
|
55
|
+
*/
|
|
56
|
+
const EditorHooks = {
|
|
57
|
+
useBlockNav(refs) {
|
|
58
|
+
// REFS
|
|
59
|
+
const bCursor = useRef(null);
|
|
60
|
+
const vCursor = useRef(0);
|
|
61
|
+
const vPending = useRef(null);
|
|
62
|
+
const restoreCursor = useRef(0);
|
|
63
|
+
// EFFECTS
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const EDITABLE = ['input', 'textarea', 'select', '[contenteditable="true"]', '[contenteditable="plaintext-only"]', '.cm-editor', 'td', 'th'].join(', ');
|
|
66
|
+
const INTERACTIVE = ['button', 'a[href]', '[role="button"]', '[role="tab"]', '[tabindex]:not([tabindex="-1"])', '[data-tool-cell]'].join(', ');
|
|
67
|
+
/**
|
|
68
|
+
* Returns the root.
|
|
69
|
+
*
|
|
70
|
+
* @returns root.
|
|
71
|
+
*/
|
|
72
|
+
const root = () => refs.current?.body ?? null;
|
|
73
|
+
/**
|
|
74
|
+
* Checks whether the value is editable.
|
|
75
|
+
*
|
|
76
|
+
* @returns editable.
|
|
77
|
+
*/
|
|
78
|
+
const editable = () => root()?.querySelector('[contenteditable="true"]') ?? null;
|
|
79
|
+
/**
|
|
80
|
+
* Gets active editable.
|
|
81
|
+
*
|
|
82
|
+
* @returns resolved value.
|
|
83
|
+
*/
|
|
84
|
+
const getActiveEditable = () => {
|
|
85
|
+
if (bCursor.current)
|
|
86
|
+
return bCursor.current.dec.parentElement?.closest('[contenteditable="true"], [contenteditable="plaintext-only"]') ?? editable();
|
|
87
|
+
const sel = window.getSelection();
|
|
88
|
+
if (sel?.anchorNode) {
|
|
89
|
+
const ce = (sel.anchorNode instanceof Element ? sel.anchorNode : sel.anchorNode.parentElement)?.closest('[contenteditable="true"], [contenteditable="plaintext-only"]');
|
|
90
|
+
if (ce)
|
|
91
|
+
return ce;
|
|
92
|
+
}
|
|
93
|
+
return editable();
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Checks whether local text.
|
|
97
|
+
*
|
|
98
|
+
* @param node - Value passed to the helper.
|
|
99
|
+
*
|
|
100
|
+
* @returns True when the helper predicate succeeds.
|
|
101
|
+
*/
|
|
102
|
+
const isLocalText = (node) => {
|
|
103
|
+
const ce = getActiveEditable();
|
|
104
|
+
if (!node || !ce)
|
|
105
|
+
return false;
|
|
106
|
+
const el = node instanceof Element ? node : node.parentElement;
|
|
107
|
+
if (!el)
|
|
108
|
+
return false;
|
|
109
|
+
const childDec = el.closest('[data-lexical-decorator="true"]');
|
|
110
|
+
return !childDec || !ce.contains(childDec);
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Sets block cursor active.
|
|
114
|
+
*
|
|
115
|
+
* @param active - Value passed to the helper.
|
|
116
|
+
*/
|
|
117
|
+
const setBlockCursorActive = (a) => {
|
|
118
|
+
if (!refs.current?.body)
|
|
119
|
+
return;
|
|
120
|
+
if (a)
|
|
121
|
+
refs.current.body.dataset.docsBlockCursorActive = 'true';
|
|
122
|
+
else
|
|
123
|
+
delete refs.current.body.dataset.docsBlockCursorActive;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Restores the restore Saved Selection.
|
|
127
|
+
*
|
|
128
|
+
* @param _reason - Value passed to the helper.
|
|
129
|
+
*/
|
|
130
|
+
const restoreSavedSelection = (_reason) => {
|
|
131
|
+
const saved = refs.current.selection ?? $cursorPosition.get() ?? $cursor.get() ?? $cursorGhost.get();
|
|
132
|
+
if (!saved)
|
|
133
|
+
return;
|
|
134
|
+
const view = codeViewOf();
|
|
135
|
+
if (view && view.dom.offsetParent !== null && saved.code) {
|
|
136
|
+
$cursorTrack.set({ anchor: saved.code.from, collapsed: saved.code.from === saved.code.to, focus: saved.code.to, range: [saved.code.from, saved.code.to], visible: true });
|
|
137
|
+
$cursorPosition.set({ code: { from: saved.code.from, to: saved.code.to }, dom: null, visible: true });
|
|
138
|
+
$cursor.set({ code: { from: saved.code.from, to: saved.code.to }, dom: null, visible: true });
|
|
139
|
+
$cursorGhost.set({ code: { from: saved.code.from, to: saved.code.to }, dom: null, visible: true });
|
|
140
|
+
refs.current.selection = { ...saved, code: { from: saved.code.from, to: saved.code.to }, dom: null, visible: true };
|
|
141
|
+
view.dispatch({ selection: { anchor: saved.code.from, head: saved.code.to }, scrollIntoView: false });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const body = refs.current.body;
|
|
145
|
+
if (!body)
|
|
146
|
+
return;
|
|
147
|
+
const sel = window.getSelection();
|
|
148
|
+
if (!sel)
|
|
149
|
+
return;
|
|
150
|
+
/**
|
|
151
|
+
* Returns the pick path.
|
|
152
|
+
*
|
|
153
|
+
* @param path - Path to process.
|
|
154
|
+
* @param offset - Value passed to the helper.
|
|
155
|
+
*
|
|
156
|
+
* @returns pick Path.
|
|
157
|
+
*/
|
|
158
|
+
const pickPath = (path, offset) => {
|
|
159
|
+
if (!path?.length)
|
|
160
|
+
return null;
|
|
161
|
+
let node = body;
|
|
162
|
+
for (const idx of path) {
|
|
163
|
+
if (!node?.childNodes[idx]) {
|
|
164
|
+
node = null;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
node = node.childNodes[idx] ?? null;
|
|
168
|
+
}
|
|
169
|
+
if (!(node instanceof Text))
|
|
170
|
+
return null;
|
|
171
|
+
const range = document.createRange();
|
|
172
|
+
range.setStart(node, Math.min(offset ?? 0, node.textContent?.length ?? 0));
|
|
173
|
+
range.collapse(true);
|
|
174
|
+
return range;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Returns the pick text.
|
|
178
|
+
*
|
|
179
|
+
* @param text - Text content to process.
|
|
180
|
+
* @param offset - Value passed to the helper.
|
|
181
|
+
*
|
|
182
|
+
* @returns pick Text.
|
|
183
|
+
*/
|
|
184
|
+
const pickText = (text, offset) => {
|
|
185
|
+
if (!text)
|
|
186
|
+
return null;
|
|
187
|
+
const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
|
|
188
|
+
acceptNode: node => (node.textContent?.includes(text) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP),
|
|
189
|
+
});
|
|
190
|
+
let node = walker.nextNode();
|
|
191
|
+
while (node) {
|
|
192
|
+
if (node instanceof Text) {
|
|
193
|
+
const i = node.textContent?.indexOf(text) ?? -1;
|
|
194
|
+
if (i !== -1) {
|
|
195
|
+
const range = document.createRange();
|
|
196
|
+
range.setStart(node, Math.min(i + (offset ?? 0), node.textContent?.length ?? 0));
|
|
197
|
+
range.collapse(true);
|
|
198
|
+
return range;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
node = walker.nextNode();
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
};
|
|
205
|
+
const n = pickPath(saved.anchorPath ?? undefined, saved.anchorOffset) ??
|
|
206
|
+
pickText(saved.anchorText, saved.anchorOffset) ??
|
|
207
|
+
pickPath(saved.focusPath ?? undefined, saved.focusOffset) ??
|
|
208
|
+
pickText(saved.focusText, saved.focusOffset) ??
|
|
209
|
+
(saved.dom && body.contains(saved.dom.commonAncestorContainer) ? saved.dom.cloneRange() : null);
|
|
210
|
+
if (!n)
|
|
211
|
+
return;
|
|
212
|
+
sel.removeAllRanges();
|
|
213
|
+
sel.addRange(n.cloneRange());
|
|
214
|
+
const rect = n.getBoundingClientRect();
|
|
215
|
+
const box = rect.width || rect.height ? { height: Math.max(16, rect.height || 0), left: rect.left, top: rect.top, width: Math.max(2, rect.width || 0) } : saved.box;
|
|
216
|
+
const anchorLinear = typeof saved.anchorLinear === 'number' ? saved.anchorLinear : captureLinearOffset(body, n.startContainer, n.startOffset);
|
|
217
|
+
const focusLinear = typeof saved.focusLinear === 'number' ? saved.focusLinear : anchorLinear;
|
|
218
|
+
$cursorTrack.set({
|
|
219
|
+
anchor: anchorLinear ?? 0,
|
|
220
|
+
box,
|
|
221
|
+
collapsed: n.collapsed,
|
|
222
|
+
focus: focusLinear ?? anchorLinear ?? 0,
|
|
223
|
+
range: [anchorLinear ?? 0, focusLinear ?? anchorLinear ?? 0],
|
|
224
|
+
visible: true,
|
|
225
|
+
});
|
|
226
|
+
$cursorPosition.set({
|
|
227
|
+
anchorOffset: saved.anchorOffset,
|
|
228
|
+
anchorPath: saved.anchorPath,
|
|
229
|
+
anchorText: saved.anchorText,
|
|
230
|
+
anchorLinear,
|
|
231
|
+
box,
|
|
232
|
+
collapsed: n.collapsed,
|
|
233
|
+
code: null,
|
|
234
|
+
dom: n.cloneRange(),
|
|
235
|
+
focusOffset: saved.focusOffset,
|
|
236
|
+
focusPath: saved.focusPath,
|
|
237
|
+
focusText: saved.focusText,
|
|
238
|
+
focusLinear,
|
|
239
|
+
text: saved.text,
|
|
240
|
+
visible: true,
|
|
241
|
+
});
|
|
242
|
+
$cursor.set({
|
|
243
|
+
anchorOffset: saved.anchorOffset,
|
|
244
|
+
anchorPath: saved.anchorPath,
|
|
245
|
+
anchorText: saved.anchorText,
|
|
246
|
+
anchorLinear,
|
|
247
|
+
box,
|
|
248
|
+
collapsed: n.collapsed,
|
|
249
|
+
code: null,
|
|
250
|
+
dom: n.cloneRange(),
|
|
251
|
+
focusOffset: saved.focusOffset,
|
|
252
|
+
focusPath: saved.focusPath,
|
|
253
|
+
focusText: saved.focusText,
|
|
254
|
+
focusLinear,
|
|
255
|
+
text: saved.text,
|
|
256
|
+
visible: true,
|
|
257
|
+
});
|
|
258
|
+
$cursorGhost.set({
|
|
259
|
+
anchorOffset: saved.anchorOffset,
|
|
260
|
+
anchorPath: saved.anchorPath,
|
|
261
|
+
anchorText: saved.anchorText,
|
|
262
|
+
anchorLinear,
|
|
263
|
+
box,
|
|
264
|
+
collapsed: n.collapsed,
|
|
265
|
+
code: null,
|
|
266
|
+
dom: n.cloneRange(),
|
|
267
|
+
focusOffset: saved.focusOffset,
|
|
268
|
+
focusPath: saved.focusPath,
|
|
269
|
+
focusText: saved.focusText,
|
|
270
|
+
focusLinear,
|
|
271
|
+
text: saved.text,
|
|
272
|
+
visible: true,
|
|
273
|
+
});
|
|
274
|
+
refs.current.selection = { ...saved, anchorLinear, box, collapsed: n.collapsed, code: null, dom: n.cloneRange(), focusLinear, visible: true };
|
|
275
|
+
setBlockCursorActive(Boolean(bCursor.current));
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* Checks whether toolbar target.
|
|
279
|
+
*
|
|
280
|
+
* @param target - Value passed to the helper.
|
|
281
|
+
*
|
|
282
|
+
* @returns True when the helper predicate succeeds.
|
|
283
|
+
*/
|
|
284
|
+
const isToolbarTgt = (tgt) => !!tgt?.closest('[data-docs-main-toolbar], [data-docs-toolbar-frame], [data-toolbar="true"], [role="toolbar"], [role="dialog"], [role="menu"], [role="menuitem"], [data-radix-popper-content-wrapper]');
|
|
285
|
+
/**
|
|
286
|
+
* Snapshot selection.
|
|
287
|
+
*
|
|
288
|
+
*/
|
|
289
|
+
const snapshotSelection = () => {
|
|
290
|
+
const sel = window.getSelection();
|
|
291
|
+
const view = codeViewOf();
|
|
292
|
+
if (view && view.dom.offsetParent !== null) {
|
|
293
|
+
refs.current.selection = { code: { from: view.state.selection.main.from, to: view.state.selection.main.to }, collapsed: view.state.selection.main.empty, dom: null, visible: true };
|
|
294
|
+
$cursorTrack.set({
|
|
295
|
+
anchor: view.state.selection.main.from,
|
|
296
|
+
collapsed: view.state.selection.main.empty,
|
|
297
|
+
focus: view.state.selection.main.to,
|
|
298
|
+
range: [view.state.selection.main.from, view.state.selection.main.to],
|
|
299
|
+
visible: true,
|
|
300
|
+
});
|
|
301
|
+
$cursorPosition.set({ code: { from: view.state.selection.main.from, to: view.state.selection.main.to }, collapsed: view.state.selection.main.empty, dom: null, visible: true });
|
|
302
|
+
$cursor.set({ code: { from: view.state.selection.main.from, to: view.state.selection.main.to }, collapsed: view.state.selection.main.empty, dom: null, visible: true });
|
|
303
|
+
$cursorGhost.set({ code: { from: view.state.selection.main.from, to: view.state.selection.main.to }, collapsed: view.state.selection.main.empty, dom: null, visible: true });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (sel?.rangeCount && sel.anchorNode) {
|
|
307
|
+
const ce = (sel.anchorNode instanceof Element ? sel.anchorNode : sel.anchorNode.parentElement)?.closest('[contenteditable="true"], [contenteditable="plaintext-only"]');
|
|
308
|
+
if (ce && refs.current.body?.contains(ce)) {
|
|
309
|
+
const dom = sel.getRangeAt(0).cloneRange();
|
|
310
|
+
const rect = dom.getBoundingClientRect();
|
|
311
|
+
const anchorPath = captureNodePath(refs.current.body, sel.anchorNode);
|
|
312
|
+
const focusPath = captureNodePath(refs.current.body, sel.focusNode);
|
|
313
|
+
const anchorLinear = captureLinearOffset(refs.current.body, sel.anchorNode, sel.anchorOffset);
|
|
314
|
+
const focusLinear = captureLinearOffset(refs.current.body, sel.focusNode, sel.focusOffset);
|
|
315
|
+
$cursorTrack.set({
|
|
316
|
+
anchor: anchorLinear ?? 0,
|
|
317
|
+
box: rect.width || rect.height ? { height: Math.max(16, rect.height || 0), left: rect.left, top: rect.top, width: Math.max(2, rect.width || 0) } : undefined,
|
|
318
|
+
collapsed: sel.isCollapsed,
|
|
319
|
+
focus: focusLinear ?? anchorLinear ?? 0,
|
|
320
|
+
range: [anchorLinear ?? 0, focusLinear ?? anchorLinear ?? 0],
|
|
321
|
+
visible: true,
|
|
322
|
+
});
|
|
323
|
+
refs.current.selection = {
|
|
324
|
+
anchorOffset: sel.anchorOffset,
|
|
325
|
+
anchorPath,
|
|
326
|
+
anchorText: sel.anchorNode?.textContent || undefined,
|
|
327
|
+
anchorLinear,
|
|
328
|
+
box: rect.width || rect.height ? { height: Math.max(16, rect.height || 0), left: rect.left, top: rect.top, width: Math.max(2, rect.width || 0) } : undefined,
|
|
329
|
+
collapsed: sel.isCollapsed,
|
|
330
|
+
code: null,
|
|
331
|
+
dom,
|
|
332
|
+
focusOffset: sel.focusOffset,
|
|
333
|
+
focusPath,
|
|
334
|
+
focusText: sel.focusNode?.textContent || undefined,
|
|
335
|
+
focusLinear,
|
|
336
|
+
text: sel.toString(),
|
|
337
|
+
visible: true,
|
|
338
|
+
};
|
|
339
|
+
$cursorPosition.set({
|
|
340
|
+
anchorOffset: sel.anchorOffset,
|
|
341
|
+
anchorPath,
|
|
342
|
+
anchorText: sel.anchorNode?.textContent || undefined,
|
|
343
|
+
anchorLinear,
|
|
344
|
+
box: rect.width || rect.height ? { height: Math.max(16, rect.height || 0), left: rect.left, top: rect.top, width: Math.max(2, rect.width || 0) } : undefined,
|
|
345
|
+
collapsed: sel.isCollapsed,
|
|
346
|
+
code: null,
|
|
347
|
+
dom,
|
|
348
|
+
focusOffset: sel.focusOffset,
|
|
349
|
+
focusPath,
|
|
350
|
+
focusText: sel.focusNode?.textContent || undefined,
|
|
351
|
+
focusLinear,
|
|
352
|
+
text: sel.toString(),
|
|
353
|
+
visible: true,
|
|
354
|
+
});
|
|
355
|
+
$cursor.set({
|
|
356
|
+
anchorOffset: sel.anchorOffset,
|
|
357
|
+
anchorPath,
|
|
358
|
+
anchorText: sel.anchorNode?.textContent || undefined,
|
|
359
|
+
anchorLinear,
|
|
360
|
+
box: rect.width || rect.height ? { height: Math.max(16, rect.height || 0), left: rect.left, top: rect.top, width: Math.max(2, rect.width || 0) } : undefined,
|
|
361
|
+
collapsed: sel.isCollapsed,
|
|
362
|
+
code: null,
|
|
363
|
+
dom,
|
|
364
|
+
focusOffset: sel.focusOffset,
|
|
365
|
+
focusPath,
|
|
366
|
+
focusText: sel.focusNode?.textContent || undefined,
|
|
367
|
+
focusLinear,
|
|
368
|
+
text: sel.toString(),
|
|
369
|
+
visible: true,
|
|
370
|
+
});
|
|
371
|
+
$cursorGhost.set({
|
|
372
|
+
anchorOffset: sel.anchorOffset,
|
|
373
|
+
anchorPath,
|
|
374
|
+
anchorText: sel.anchorNode?.textContent || undefined,
|
|
375
|
+
anchorLinear,
|
|
376
|
+
box: rect.width || rect.height ? { height: Math.max(16, rect.height || 0), left: rect.left, top: rect.top, width: Math.max(2, rect.width || 0) } : undefined,
|
|
377
|
+
collapsed: sel.isCollapsed,
|
|
378
|
+
code: null,
|
|
379
|
+
dom,
|
|
380
|
+
focusOffset: sel.focusOffset,
|
|
381
|
+
focusPath,
|
|
382
|
+
focusText: sel.focusNode?.textContent || undefined,
|
|
383
|
+
focusLinear,
|
|
384
|
+
text: sel.toString(),
|
|
385
|
+
visible: true,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Returns the clear.
|
|
392
|
+
*
|
|
393
|
+
*/
|
|
394
|
+
const clear = () => {
|
|
395
|
+
window.cancelAnimationFrame(vCursor.current);
|
|
396
|
+
vPending.current = null;
|
|
397
|
+
if (bCursor.current) {
|
|
398
|
+
bCursor.current.wrapper.removeAttribute('data-docs-block-cursor');
|
|
399
|
+
setBlockCursorActive(false);
|
|
400
|
+
bCursor.current = null;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* Returns the restore.
|
|
405
|
+
*
|
|
406
|
+
* @returns True when the helper predicate succeeds.
|
|
407
|
+
*/
|
|
408
|
+
const restore = () => {
|
|
409
|
+
const { current: cur } = bCursor;
|
|
410
|
+
const body = root();
|
|
411
|
+
if (!cur || !body)
|
|
412
|
+
return false;
|
|
413
|
+
const matches = Array.from(body.querySelectorAll('[data-docs-editor-block]')).filter(w => w.getAttribute('data-docs-editor-block') === cur.label);
|
|
414
|
+
const nWrapper = matches.find(w => {
|
|
415
|
+
if (!cur.gId)
|
|
416
|
+
return false;
|
|
417
|
+
return (w.matches('[data-docs-focus-group-id]') ? w : w.querySelector('[data-docs-focus-group-id]'))?.getAttribute('data-docs-focus-group-id') === cur.gId;
|
|
418
|
+
}) ?? matches[0];
|
|
419
|
+
if (!nWrapper)
|
|
420
|
+
return false;
|
|
421
|
+
const n = nWrapper.closest('[data-lexical-decorator="true"]') ?? nWrapper;
|
|
422
|
+
nWrapper.dataset.docsBlockCursor = 'true';
|
|
423
|
+
setBlockCursorActive(true);
|
|
424
|
+
bCursor.current = { ...cur, dec: n, wrapper: nWrapper };
|
|
425
|
+
// natively select the node so Lexical restores its internal state
|
|
426
|
+
const selection = { body: getActiveEditable(), range: window.getSelection() };
|
|
427
|
+
if (selection.body && selection.range) {
|
|
428
|
+
const range = document.createRange();
|
|
429
|
+
range.selectNode(n);
|
|
430
|
+
selection.range.removeAllRanges();
|
|
431
|
+
selection.range.addRange(range);
|
|
432
|
+
}
|
|
433
|
+
const ghost = document.createRange();
|
|
434
|
+
ghost.selectNode(n);
|
|
435
|
+
$cursor.set({ code: null, dom: ghost, visible: true });
|
|
436
|
+
return true;
|
|
437
|
+
};
|
|
438
|
+
/**
|
|
439
|
+
* G dec.
|
|
440
|
+
*
|
|
441
|
+
* @param dec - Value passed to the helper.
|
|
442
|
+
*
|
|
443
|
+
* @returns g Dec.
|
|
444
|
+
*/
|
|
445
|
+
const gDec = (dec) => {
|
|
446
|
+
const body = root();
|
|
447
|
+
const marker = dec.matches('[data-docs-focus-group-id]') ? dec : dec.querySelector('[data-docs-focus-group-id]');
|
|
448
|
+
const gId = marker?.getAttribute('data-docs-focus-group-id');
|
|
449
|
+
if (!body || !gId || marker?.getAttribute('data-docs-focus-group') !== 'member')
|
|
450
|
+
return dec;
|
|
451
|
+
const gRoot = body.querySelector(`[data-docs-focus-group="root"][data-docs-focus-group-id="${gId}"]`);
|
|
452
|
+
return gRoot?.closest('[data-lexical-decorator="true"]') ?? gRoot ?? dec;
|
|
453
|
+
};
|
|
454
|
+
/**
|
|
455
|
+
* G decs.
|
|
456
|
+
*
|
|
457
|
+
* @param _vertical - Value passed to the helper.
|
|
458
|
+
*
|
|
459
|
+
* @returns g Decs.
|
|
460
|
+
*/
|
|
461
|
+
const gDecs = (_vertical = false) => {
|
|
462
|
+
const ce = getActiveEditable();
|
|
463
|
+
if (!ce)
|
|
464
|
+
return [];
|
|
465
|
+
const seen = new Set();
|
|
466
|
+
return [
|
|
467
|
+
...Array.from(ce.querySelectorAll('[data-lexical-decorator="true"]')),
|
|
468
|
+
...Array.from(ce.querySelectorAll('[data-docs-editor-block]')).filter(b => !b.closest('[data-lexical-decorator="true"]')),
|
|
469
|
+
]
|
|
470
|
+
.filter(e => {
|
|
471
|
+
if (e.parentElement?.closest('[contenteditable="true"], [contenteditable="plaintext-only"]') !== ce)
|
|
472
|
+
return false;
|
|
473
|
+
return true;
|
|
474
|
+
})
|
|
475
|
+
.map(gDec)
|
|
476
|
+
.filter(d => {
|
|
477
|
+
if (seen.has(d))
|
|
478
|
+
return false;
|
|
479
|
+
seen.add(d);
|
|
480
|
+
return true;
|
|
481
|
+
})
|
|
482
|
+
.sort((a, b) => {
|
|
483
|
+
const pos = a.compareDocumentPosition(b);
|
|
484
|
+
if (pos & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
485
|
+
return -1;
|
|
486
|
+
if (pos & Node.DOCUMENT_POSITION_PRECEDING)
|
|
487
|
+
return 1;
|
|
488
|
+
return 0;
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
492
|
+
* Returns the focus target.
|
|
493
|
+
*
|
|
494
|
+
* @param tgt - Value passed to the helper.
|
|
495
|
+
* @param side - Value passed to the helper.
|
|
496
|
+
*
|
|
497
|
+
* @returns True when the helper predicate succeeds.
|
|
498
|
+
*/
|
|
499
|
+
const focusTgt = (tgt, side) => {
|
|
500
|
+
tgt.focus({ preventScroll: true });
|
|
501
|
+
if (!tgt.matches('[contenteditable="true"], [contenteditable="plaintext-only"]'))
|
|
502
|
+
return document.activeElement === tgt;
|
|
503
|
+
const sel = window.getSelection();
|
|
504
|
+
if (!sel)
|
|
505
|
+
return false;
|
|
506
|
+
const walker = document.createTreeWalker(tgt, NodeFilter.SHOW_TEXT, {
|
|
507
|
+
acceptNode: n => (n.textContent?.replace(TerraQualityRegex.REGEX_U200B_G_REGEX(), '').trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP),
|
|
508
|
+
});
|
|
509
|
+
const texts = [];
|
|
510
|
+
let node = walker.nextNode();
|
|
511
|
+
while (node) {
|
|
512
|
+
if (node instanceof Text)
|
|
513
|
+
texts.push(node);
|
|
514
|
+
node = walker.nextNode();
|
|
515
|
+
}
|
|
516
|
+
const range = document.createRange();
|
|
517
|
+
const text = side === 'right' ? texts[0] : texts[texts.length - 1];
|
|
518
|
+
if (text)
|
|
519
|
+
range.setStart(text, side === 'right' ? 0 : (text.textContent?.length ?? 0));
|
|
520
|
+
else
|
|
521
|
+
range.selectNodeContents(tgt);
|
|
522
|
+
range.collapse(true);
|
|
523
|
+
sel.removeAllRanges();
|
|
524
|
+
sel.addRange(range);
|
|
525
|
+
return true;
|
|
526
|
+
};
|
|
527
|
+
/**
|
|
528
|
+
* Returns the focusables.
|
|
529
|
+
*
|
|
530
|
+
* @param wrapper - Value passed to the helper.
|
|
531
|
+
* @param pref - Value passed to the helper.
|
|
532
|
+
*
|
|
533
|
+
* @returns focusables.
|
|
534
|
+
*/
|
|
535
|
+
const focusables = (wrapper, pref = false) => {
|
|
536
|
+
const seen = new Set();
|
|
537
|
+
const gId = wrapper.getAttribute('data-docs-focus-group-id');
|
|
538
|
+
return Array.from(wrapper.querySelectorAll(pref ? '[data-docs-focus-entry]' : [EDITABLE, INTERACTIVE].join(', '))).filter(n => {
|
|
539
|
+
if (seen.has(n) || !n.isConnected)
|
|
540
|
+
return false;
|
|
541
|
+
seen.add(n);
|
|
542
|
+
if (pref) {
|
|
543
|
+
const owner = n.closest('[data-docs-editor-block]');
|
|
544
|
+
if (owner && owner !== wrapper && (!gId || owner.getAttribute('data-docs-focus-group-id') !== gId))
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
if ('disabled' in n && n.disabled)
|
|
548
|
+
return false;
|
|
549
|
+
if (n.getAttribute('aria-hidden') === 'true' || n.closest('[aria-hidden="true"]'))
|
|
550
|
+
return false;
|
|
551
|
+
return !!n.getClientRects().length;
|
|
552
|
+
});
|
|
553
|
+
};
|
|
554
|
+
/**
|
|
555
|
+
* Nearest adjacent.
|
|
556
|
+
*
|
|
557
|
+
* @param dir - Directory entry to read.
|
|
558
|
+
* @param vertical - Value passed to the helper.
|
|
559
|
+
*
|
|
560
|
+
* @returns nearest Adjacent.
|
|
561
|
+
*/
|
|
562
|
+
const nearestAdjacent = (dir, vertical = false) => {
|
|
563
|
+
const range = getCollapsedRange();
|
|
564
|
+
if (!range)
|
|
565
|
+
return null;
|
|
566
|
+
const decs = gDecs(vertical);
|
|
567
|
+
if (dir === 'right') {
|
|
568
|
+
return (decs.find(d => {
|
|
569
|
+
const edge = document.createRange();
|
|
570
|
+
edge.setStartBefore(d);
|
|
571
|
+
edge.collapse(true);
|
|
572
|
+
return range.compareBoundaryPoints(Range.START_TO_START, edge) <= 0;
|
|
573
|
+
}) ?? null);
|
|
574
|
+
}
|
|
575
|
+
for (let i = decs.length - 1; i >= 0; i--) {
|
|
576
|
+
const d = decs[i];
|
|
577
|
+
const edge = document.createRange();
|
|
578
|
+
edge.setStartAfter(d);
|
|
579
|
+
edge.collapse(true);
|
|
580
|
+
if (range.compareBoundaryPoints(Range.START_TO_START, edge) >= 0)
|
|
581
|
+
return d;
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
};
|
|
585
|
+
/**
|
|
586
|
+
* Caret rect.
|
|
587
|
+
*
|
|
588
|
+
* @param dir - Directory entry to read.
|
|
589
|
+
*
|
|
590
|
+
* @returns caret Rect.
|
|
591
|
+
*/
|
|
592
|
+
const caretRect = (dir) => {
|
|
593
|
+
const range = getCollapsedRange();
|
|
594
|
+
if (!range)
|
|
595
|
+
return null;
|
|
596
|
+
const rects = Array.from(range.getClientRects());
|
|
597
|
+
const rect = (dir === 'right' ? rects[rects.length - 1] : rects[0]) || range.getBoundingClientRect();
|
|
598
|
+
return rect.width || rect.height ? { bottom: rect.bottom, top: rect.top } : null;
|
|
599
|
+
};
|
|
600
|
+
/**
|
|
601
|
+
* Passed dec.
|
|
602
|
+
*
|
|
603
|
+
* @param dec - Value passed to the helper.
|
|
604
|
+
* @param dir - Directory entry to read.
|
|
605
|
+
* @param beforeAnchor - Value passed to the helper.
|
|
606
|
+
* @param beforeBlock - Value passed to the helper.
|
|
607
|
+
* @param before - Value passed to the helper.
|
|
608
|
+
* @param aRect - Value passed to the helper.
|
|
609
|
+
*
|
|
610
|
+
* @returns True when the helper predicate succeeds.
|
|
611
|
+
*/
|
|
612
|
+
const passedDec = (dec, dir, beforeAnchor, beforeBlock, before, aRect) => {
|
|
613
|
+
const select = window.getSelection();
|
|
614
|
+
if (!select?.rangeCount || !select.isCollapsed || !before || !aRect)
|
|
615
|
+
return false;
|
|
616
|
+
const block = (select.anchorNode instanceof Element ? select.anchorNode : select.anchorNode?.parentElement)?.closest('p, li, blockquote, h2, h3, h4, h5, h6, td, th') ?? null;
|
|
617
|
+
if (select.anchorNode === beforeAnchor && block === beforeBlock)
|
|
618
|
+
return false;
|
|
619
|
+
// Ignore if we've escaped local boundary limits during native modification
|
|
620
|
+
if (block && !isLocalText(block))
|
|
621
|
+
return false;
|
|
622
|
+
const range = select.getRangeAt(0);
|
|
623
|
+
const edge = document.createRange();
|
|
624
|
+
const box = dec.getBoundingClientRect();
|
|
625
|
+
const bRect = caretRect(dir);
|
|
626
|
+
if (!bRect)
|
|
627
|
+
return false;
|
|
628
|
+
if (dir === 'right')
|
|
629
|
+
edge.setStartAfter(dec);
|
|
630
|
+
else
|
|
631
|
+
edge.setStartBefore(dec);
|
|
632
|
+
edge.collapse(true);
|
|
633
|
+
const leeway = 10;
|
|
634
|
+
// prettier-ignore
|
|
635
|
+
return dir === 'right'
|
|
636
|
+
? before.compareBoundaryPoints(Range.START_TO_START, edge) < 0 && range.compareBoundaryPoints(Range.START_TO_START, edge) >= 0 && aRect.bottom < box.top + leeway && bRect.bottom >= box.top - leeway
|
|
637
|
+
: before.compareBoundaryPoints(Range.START_TO_START, edge) > 0 && range.compareBoundaryPoints(Range.START_TO_START, edge) <= 0 && aRect.top > box.bottom - leeway && bRect.top <= box.bottom + leeway;
|
|
638
|
+
};
|
|
639
|
+
/**
|
|
640
|
+
* Selected dec.
|
|
641
|
+
*
|
|
642
|
+
* @param _vertical - Value passed to the helper.
|
|
643
|
+
*
|
|
644
|
+
* @returns selected Dec.
|
|
645
|
+
*/
|
|
646
|
+
const selectedDec = (_vertical = false) => {
|
|
647
|
+
const ce = getActiveEditable();
|
|
648
|
+
if (!ce)
|
|
649
|
+
return null;
|
|
650
|
+
const range = getCollapsedRange();
|
|
651
|
+
if (!range || !ce.contains(range.startContainer))
|
|
652
|
+
return null;
|
|
653
|
+
const dec = (range.startContainer instanceof Element ? range.startContainer : range.startContainer.parentElement)?.closest('[data-lexical-decorator="true"]');
|
|
654
|
+
if (!dec || !ce.contains(dec) || dec === ce.closest('[data-lexical-decorator="true"]'))
|
|
655
|
+
return null;
|
|
656
|
+
return gDec(dec);
|
|
657
|
+
};
|
|
658
|
+
/**
|
|
659
|
+
* Returns the focus block.
|
|
660
|
+
*
|
|
661
|
+
* @param dec - Value passed to the helper.
|
|
662
|
+
* @param dir - Directory entry to read.
|
|
663
|
+
*/
|
|
664
|
+
const focusBlock = (dec, dir) => {
|
|
665
|
+
const cells = Array.from(dec.querySelectorAll('th, td')).filter(c => c.isContentEditable && c.getClientRects().length);
|
|
666
|
+
if (cells.length) {
|
|
667
|
+
clear();
|
|
668
|
+
focusTgt(dir === 'right' ? cells[0] : cells[cells.length - 1], dir === 'right' ? 'right' : 'left');
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
focus(dec, dir === 'right' ? 'left' : 'right');
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* Returns the focus.
|
|
675
|
+
*
|
|
676
|
+
* @param dec - Value passed to the helper.
|
|
677
|
+
* @param side - Value passed to the helper.
|
|
678
|
+
*/
|
|
679
|
+
const focus = (dec, side) => {
|
|
680
|
+
const tgt = gDec(dec);
|
|
681
|
+
const wrapper = tgt.querySelector('[data-docs-editor-block]') ?? tgt;
|
|
682
|
+
const cursorMetadata = {
|
|
683
|
+
marker: wrapper.matches('[data-docs-focus-group-id]') ? wrapper : wrapper.querySelector('[data-docs-focus-group-id]'),
|
|
684
|
+
label: wrapper.getAttribute('data-docs-editor-block') ?? 'Block',
|
|
685
|
+
};
|
|
686
|
+
clear();
|
|
687
|
+
const body = getActiveEditable();
|
|
688
|
+
const sel = window.getSelection();
|
|
689
|
+
if (body && sel) {
|
|
690
|
+
body.focus({ preventScroll: true });
|
|
691
|
+
const range = document.createRange();
|
|
692
|
+
if (side === 'left')
|
|
693
|
+
range.setStartBefore(tgt);
|
|
694
|
+
else
|
|
695
|
+
range.setStartAfter(tgt);
|
|
696
|
+
range.collapse(true);
|
|
697
|
+
sel.removeAllRanges();
|
|
698
|
+
sel.addRange(range);
|
|
699
|
+
}
|
|
700
|
+
wrapper.dataset.docsBlockCursor = 'true';
|
|
701
|
+
setBlockCursorActive(true);
|
|
702
|
+
bCursor.current = { dec: tgt, gId: cursorMetadata.marker?.getAttribute('data-docs-focus-group-id') ?? null, label: cursorMetadata.label, side, wrapper };
|
|
703
|
+
document.dispatchEvent(new CustomEvent('docs:block-selected', { detail: { block: wrapper } }));
|
|
704
|
+
};
|
|
705
|
+
/**
|
|
706
|
+
* On key down.
|
|
707
|
+
*
|
|
708
|
+
* @param e - Value passed to the helper.
|
|
709
|
+
*/
|
|
710
|
+
const eHandlers = {
|
|
711
|
+
onKeyDown(e) {
|
|
712
|
+
try {
|
|
713
|
+
if (e.defaultPrevented || e.altKey)
|
|
714
|
+
return;
|
|
715
|
+
const tgt = e.target instanceof HTMLElement ? e.target : null;
|
|
716
|
+
const closestEditable = tgt?.closest('[contenteditable="true"]') ?? null;
|
|
717
|
+
const key = e.key;
|
|
718
|
+
const entry = tgt?.closest('[data-docs-focus-entry]') ?? null;
|
|
719
|
+
if ((key === 'Enter' || key === ' ') && entry?.matches('button, [role="button"], [role="tab"]')) {
|
|
720
|
+
e.preventDefault();
|
|
721
|
+
e.stopPropagation();
|
|
722
|
+
if (entry.getAttribute('aria-expanded') !== 'true')
|
|
723
|
+
entry.click();
|
|
724
|
+
setTimeout(() => {
|
|
725
|
+
const container = entry.getAttribute('role') === 'tab' ? entry.closest('[data-docs-focus-group="root"]') : entry.closest('[data-docs-focus-group="member"]');
|
|
726
|
+
if (container) {
|
|
727
|
+
const editors = Array.from(container.querySelectorAll('[contenteditable="true"], [contenteditable="plaintext-only"]')).filter(e => {
|
|
728
|
+
return window.getComputedStyle(e).display !== 'none' && e.offsetParent !== null;
|
|
729
|
+
});
|
|
730
|
+
if (editors.length > 0 && editors[0]) {
|
|
731
|
+
editors[0].focus({ preventScroll: true });
|
|
732
|
+
const sel = window.getSelection();
|
|
733
|
+
const range = document.createRange();
|
|
734
|
+
range.selectNodeContents(editors[0]);
|
|
735
|
+
range.collapse(true);
|
|
736
|
+
sel?.removeAllRanges();
|
|
737
|
+
sel?.addRange(range);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}, 100);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (bCursor.current && (key === 'Tab' || key === 'Enter')) {
|
|
744
|
+
e.preventDefault();
|
|
745
|
+
e.stopPropagation();
|
|
746
|
+
const cur = bCursor.current;
|
|
747
|
+
if (!cur)
|
|
748
|
+
return;
|
|
749
|
+
const tgt = (e.shiftKey ? focusables(cur.wrapper, true).at(-1) : focusables(cur.wrapper, true)[0]) ?? (e.shiftKey ? focusables(cur.wrapper).at(-1) : focusables(cur.wrapper)[0]);
|
|
750
|
+
if (!tgt) {
|
|
751
|
+
const ce = cur.wrapper.querySelector('[contenteditable="true"], [contenteditable="plaintext-only"]');
|
|
752
|
+
if (ce) {
|
|
753
|
+
clear();
|
|
754
|
+
ce.focus({ preventScroll: true });
|
|
755
|
+
}
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
clear();
|
|
759
|
+
focusTgt(tgt, e.shiftKey ? 'left' : 'right');
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
// If bCursor is active, we let standard copy/cut/paste combos fall completely
|
|
763
|
+
// through to Lexical to handle natively without intercepting!
|
|
764
|
+
if (bCursor.current && (e.metaKey || e.ctrlKey) && ['c', 'x', 'v'].includes(key.toLowerCase())) {
|
|
765
|
+
if (key.toLowerCase() === 'x')
|
|
766
|
+
requestAnimationFrame(() => {
|
|
767
|
+
clear();
|
|
768
|
+
});
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const dir = key === 'ArrowLeft' || key === 'ArrowUp' || key === 'Backspace' ? 'left' : key === 'ArrowRight' || key === 'ArrowDown' || key === 'Delete' ? 'right' : null;
|
|
772
|
+
if (!dir)
|
|
773
|
+
return;
|
|
774
|
+
const isVertical = key === 'ArrowUp' || key === 'ArrowDown';
|
|
775
|
+
if (!bCursor.current &&
|
|
776
|
+
(tgt?.closest(['input', 'textarea', 'select', '[contenteditable="plaintext-only"]', '.cm-editor', 'td', 'th', INTERACTIVE].join(', ')) ||
|
|
777
|
+
(closestEditable && closestEditable !== getActiveEditable())))
|
|
778
|
+
return;
|
|
779
|
+
if (bCursor.current) {
|
|
780
|
+
// Lexical will securely handle node deletion for Backspace/Delete, just visually clean up.
|
|
781
|
+
if (key === 'Backspace' || key === 'Delete') {
|
|
782
|
+
clear();
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
e.preventDefault();
|
|
786
|
+
e.stopPropagation();
|
|
787
|
+
const cur = bCursor.current;
|
|
788
|
+
if (!cur)
|
|
789
|
+
return;
|
|
790
|
+
const dir = key === 'ArrowLeft' || key === 'ArrowUp' ? 'left' : 'right';
|
|
791
|
+
const isVertical = key === 'ArrowUp' || key === 'ArrowDown';
|
|
792
|
+
const decs = gDecs(isVertical);
|
|
793
|
+
const i = decs.indexOf(cur.dec);
|
|
794
|
+
const adjacent = i === -1 ? null : (decs[i + (dir === 'right' ? 1 : -1)] ?? null);
|
|
795
|
+
if (isVertical &&
|
|
796
|
+
adjacent &&
|
|
797
|
+
!(() => {
|
|
798
|
+
const body = getActiveEditable();
|
|
799
|
+
if (!body)
|
|
800
|
+
return false;
|
|
801
|
+
const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
|
|
802
|
+
acceptNode: n => (n.textContent?.replace(TerraQualityRegex.REGEX_U200B_G_REGEX(), '').trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP),
|
|
803
|
+
});
|
|
804
|
+
const first = cur.dec.compareDocumentPosition(adjacent) & Node.DOCUMENT_POSITION_FOLLOWING ? cur.dec : adjacent;
|
|
805
|
+
let text = walker.nextNode();
|
|
806
|
+
while (text) {
|
|
807
|
+
if (text instanceof Text && isLocalText(text))
|
|
808
|
+
if (first.compareDocumentPosition(text) & Node.DOCUMENT_POSITION_FOLLOWING &&
|
|
809
|
+
(first === cur.dec ? adjacent : cur.dec).compareDocumentPosition(text) & Node.DOCUMENT_POSITION_PRECEDING)
|
|
810
|
+
return true;
|
|
811
|
+
text = walker.nextNode();
|
|
812
|
+
}
|
|
813
|
+
return false;
|
|
814
|
+
})()) {
|
|
815
|
+
focusBlock(adjacent, dir);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const body = getActiveEditable();
|
|
819
|
+
const selection = window.getSelection();
|
|
820
|
+
if (body && selection) {
|
|
821
|
+
const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
|
|
822
|
+
acceptNode: n => (n.textContent?.replace(TerraQualityRegex.REGEX_U200B_G_REGEX(), '').trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP),
|
|
823
|
+
});
|
|
824
|
+
let candidate = null;
|
|
825
|
+
let node = walker.nextNode();
|
|
826
|
+
while (node) {
|
|
827
|
+
if (node instanceof Text && isLocalText(node)) {
|
|
828
|
+
const relation = cur.dec.compareDocumentPosition(node);
|
|
829
|
+
if (dir === 'right' && relation & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
830
|
+
candidate = node;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
if (dir === 'left' && relation & Node.DOCUMENT_POSITION_PRECEDING)
|
|
834
|
+
candidate = node;
|
|
835
|
+
}
|
|
836
|
+
node = walker.nextNode();
|
|
837
|
+
}
|
|
838
|
+
body.focus({ preventScroll: true });
|
|
839
|
+
const range = document.createRange();
|
|
840
|
+
if (candidate)
|
|
841
|
+
range.setStart(candidate, dir === 'right' ? 0 : (candidate.textContent?.length ?? 0));
|
|
842
|
+
else if (dir === 'left')
|
|
843
|
+
range.setStartBefore(cur.dec);
|
|
844
|
+
else
|
|
845
|
+
range.setStartAfter(cur.dec);
|
|
846
|
+
range.collapse(true);
|
|
847
|
+
selection.removeAllRanges();
|
|
848
|
+
selection.addRange(range);
|
|
849
|
+
}
|
|
850
|
+
clear();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (key !== 'ArrowLeft' && key !== 'ArrowRight' && key !== 'ArrowUp' && key !== 'ArrowDown')
|
|
854
|
+
return;
|
|
855
|
+
const dec = (() => {
|
|
856
|
+
const range = getCollapsedRange();
|
|
857
|
+
if (!range)
|
|
858
|
+
return null;
|
|
859
|
+
const decs = gDecs(isVertical);
|
|
860
|
+
let exact;
|
|
861
|
+
if (dir === 'right') {
|
|
862
|
+
exact = decs.find(d => {
|
|
863
|
+
const edge = document.createRange();
|
|
864
|
+
edge.setStartBefore(d);
|
|
865
|
+
edge.collapse(true);
|
|
866
|
+
return range.compareBoundaryPoints(Range.START_TO_START, edge) === 0 && range.compareBoundaryPoints(Range.END_TO_END, edge) === 0;
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
for (let i = decs.length - 1; i >= 0; i--) {
|
|
871
|
+
const d = decs[i];
|
|
872
|
+
const edge = document.createRange();
|
|
873
|
+
edge.setStartAfter(d);
|
|
874
|
+
edge.collapse(true);
|
|
875
|
+
if (range.compareBoundaryPoints(Range.START_TO_START, edge) === 0 && range.compareBoundaryPoints(Range.END_TO_END, edge) === 0) {
|
|
876
|
+
exact = d;
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (exact)
|
|
882
|
+
return gDec(exact);
|
|
883
|
+
const block = (range.startContainer instanceof Element ? range.startContainer : range.startContainer.parentElement)?.closest('p, li, blockquote, h2, h3, h4, h5, h6, td, th') ?? null;
|
|
884
|
+
if (!block || !isLocalText(block))
|
|
885
|
+
return null;
|
|
886
|
+
const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, {
|
|
887
|
+
acceptNode: n => (n.textContent?.replace(TerraQualityRegex.REGEX_U200B_G_REGEX(), '').trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP),
|
|
888
|
+
});
|
|
889
|
+
const textNodes = [];
|
|
890
|
+
let text = walker.nextNode();
|
|
891
|
+
while (text) {
|
|
892
|
+
if (text instanceof Text)
|
|
893
|
+
textNodes.push(text);
|
|
894
|
+
text = walker.nextNode();
|
|
895
|
+
}
|
|
896
|
+
const tgt = dir === 'right' ? textNodes[textNodes.length - 1] : textNodes[0];
|
|
897
|
+
if (!tgt)
|
|
898
|
+
return null;
|
|
899
|
+
const textRange = document.createRange();
|
|
900
|
+
textRange.setStart(tgt, dir === 'right' ? (tgt.textContent?.length ?? 0) : 0);
|
|
901
|
+
textRange.collapse(true);
|
|
902
|
+
if (range.compareBoundaryPoints(Range.START_TO_START, textRange) === 0 && range.compareBoundaryPoints(Range.END_TO_END, textRange) === 0) {
|
|
903
|
+
if (dir === 'right') {
|
|
904
|
+
return decs.find(d => !!(d.compareDocumentPosition(tgt) & Node.DOCUMENT_POSITION_PRECEDING)) ?? null;
|
|
905
|
+
}
|
|
906
|
+
for (let i = decs.length - 1; i >= 0; i--) {
|
|
907
|
+
const d = decs[i];
|
|
908
|
+
if (d.compareDocumentPosition(tgt) & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
909
|
+
return d;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return null;
|
|
913
|
+
})();
|
|
914
|
+
if (isVertical) {
|
|
915
|
+
if (dec) {
|
|
916
|
+
e.preventDefault();
|
|
917
|
+
e.stopPropagation();
|
|
918
|
+
focusBlock(dec, dir);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (!nearestAdjacent(dir, isVertical) || !window.getSelection()?.modify)
|
|
922
|
+
return;
|
|
923
|
+
e.preventDefault();
|
|
924
|
+
e.stopPropagation();
|
|
925
|
+
const adjacent = nearestAdjacent(dir, true);
|
|
926
|
+
if (!adjacent)
|
|
927
|
+
return;
|
|
928
|
+
const selection = window.getSelection();
|
|
929
|
+
const anchor = selection?.anchorNode ?? null;
|
|
930
|
+
vPending.current = {
|
|
931
|
+
beforeAnchor: anchor,
|
|
932
|
+
beforeBlock: (anchor instanceof Element ? anchor : anchor?.parentElement)?.closest('p, li, blockquote, h2, h3, h4, h5, h6, td, th') ?? null,
|
|
933
|
+
before: selection?.rangeCount && selection.isCollapsed ? selection.getRangeAt(0).cloneRange() : null,
|
|
934
|
+
beforeRect: caretRect(dir),
|
|
935
|
+
dec: adjacent,
|
|
936
|
+
dir,
|
|
937
|
+
};
|
|
938
|
+
window.cancelAnimationFrame(vCursor.current);
|
|
939
|
+
vCursor.current = window.requestAnimationFrame(() => {
|
|
940
|
+
(function settle(attempt = 0) {
|
|
941
|
+
vCursor.current = 0;
|
|
942
|
+
const pending = vPending.current;
|
|
943
|
+
if (!pending?.dec || bCursor.current)
|
|
944
|
+
return void (vPending.current = null);
|
|
945
|
+
const selected = selectedDec(true);
|
|
946
|
+
if (selected) {
|
|
947
|
+
vPending.current = null;
|
|
948
|
+
focusBlock(selected, pending.dir);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const currSelection = window.getSelection();
|
|
952
|
+
if (currSelection?.anchorNode && pending.dec.contains(currSelection.anchorNode)) {
|
|
953
|
+
vPending.current = null;
|
|
954
|
+
focusBlock(pending.dec, pending.dir);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (pending.dec && passedDec(pending.dec, pending.dir, pending.beforeAnchor, pending.beforeBlock, pending.before, pending.beforeRect)) {
|
|
958
|
+
vPending.current = null;
|
|
959
|
+
focusBlock(pending.dec, pending.dir);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (attempt >= 5)
|
|
963
|
+
return void (vPending.current = null);
|
|
964
|
+
vCursor.current = window.requestAnimationFrame(() => settle(attempt + 1));
|
|
965
|
+
})(0);
|
|
966
|
+
});
|
|
967
|
+
const verticalSelection = window.getSelection();
|
|
968
|
+
if (!verticalSelection?.rangeCount || !verticalSelection.isCollapsed || !verticalSelection.modify)
|
|
969
|
+
return;
|
|
970
|
+
verticalSelection.modify('move', dir === 'right' ? 'forward' : 'backward', 'line');
|
|
971
|
+
const pending = vPending.current;
|
|
972
|
+
if (!pending?.dec)
|
|
973
|
+
return;
|
|
974
|
+
const selected = selectedDec(true);
|
|
975
|
+
if (selected) {
|
|
976
|
+
vPending.current = null;
|
|
977
|
+
focusBlock(selected, dir);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (pending.dec && passedDec(pending.dec, pending.dir, pending.beforeAnchor, pending.beforeBlock, pending.before, pending.beforeRect)) {
|
|
981
|
+
vPending.current = null;
|
|
982
|
+
focusBlock(pending.dec, dir);
|
|
983
|
+
}
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (!dec)
|
|
987
|
+
return;
|
|
988
|
+
e.preventDefault();
|
|
989
|
+
e.stopPropagation();
|
|
990
|
+
focus(dec, dir === 'right' ? 'left' : 'right');
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
console.error('[docs:edit] Key down event failed', err);
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
/**
|
|
997
|
+
* On selection change.
|
|
998
|
+
*
|
|
999
|
+
*/
|
|
1000
|
+
onSelectionChange() {
|
|
1001
|
+
try {
|
|
1002
|
+
snapshotSelection();
|
|
1003
|
+
const sel = window.getSelection();
|
|
1004
|
+
const p = vPending.current;
|
|
1005
|
+
if (!bCursor.current && p?.dec) {
|
|
1006
|
+
const dec = selectedDec(true);
|
|
1007
|
+
if (dec) {
|
|
1008
|
+
vPending.current = null;
|
|
1009
|
+
window.cancelAnimationFrame(vCursor.current);
|
|
1010
|
+
focusBlock(dec, p.dir);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (sel?.anchorNode && p.dec.contains(sel.anchorNode)) {
|
|
1014
|
+
vPending.current = null;
|
|
1015
|
+
window.cancelAnimationFrame(vCursor.current);
|
|
1016
|
+
focusBlock(p.dec, p.dir);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (passedDec(p.dec, p.dir, p.beforeAnchor, p.beforeBlock, p.before, p.beforeRect)) {
|
|
1020
|
+
vPending.current = null;
|
|
1021
|
+
window.cancelAnimationFrame(vCursor.current);
|
|
1022
|
+
focusBlock(p.dec, p.dir);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const { current: cur } = bCursor;
|
|
1027
|
+
if (!cur)
|
|
1028
|
+
return;
|
|
1029
|
+
if (!cur.dec.isConnected || !cur.wrapper.isConnected) {
|
|
1030
|
+
if (restore())
|
|
1031
|
+
return;
|
|
1032
|
+
window.cancelAnimationFrame(restoreCursor.current);
|
|
1033
|
+
restoreCursor.current = window.requestAnimationFrame(() => {
|
|
1034
|
+
(function retryRestore(attempt = 0) {
|
|
1035
|
+
if (restore())
|
|
1036
|
+
return;
|
|
1037
|
+
if (attempt >= 12) {
|
|
1038
|
+
clear();
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
restoreCursor.current = window.requestAnimationFrame(() => retryRestore(attempt + 1));
|
|
1042
|
+
})(0);
|
|
1043
|
+
});
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (isToolbarTgt(document.activeElement)) {
|
|
1047
|
+
restoreSavedSelection('selectionchange-toolbar');
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (!sel?.rangeCount)
|
|
1051
|
+
return;
|
|
1052
|
+
if (!sel.isCollapsed) {
|
|
1053
|
+
const range = sel.getRangeAt(0);
|
|
1054
|
+
if (range.startContainer === range.endContainer && range.endOffset - range.startOffset === 1) {
|
|
1055
|
+
const selNode = range.startContainer.childNodes[range.startOffset];
|
|
1056
|
+
if (selNode && (selNode === cur.dec || cur.dec.contains(selNode) || cur.wrapper.contains(selNode)))
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (cur.dec.contains(range.startContainer) || cur.wrapper.contains(range.startContainer))
|
|
1060
|
+
return;
|
|
1061
|
+
clear();
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (!sel.anchorNode || (!root()?.contains(sel.anchorNode) && sel.anchorNode !== document.body))
|
|
1065
|
+
clear();
|
|
1066
|
+
}
|
|
1067
|
+
catch (err) {
|
|
1068
|
+
console.error('[docs:edit] Selection change event failed', err);
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
/**
|
|
1072
|
+
* On pointer down.
|
|
1073
|
+
*
|
|
1074
|
+
* @param e - Value passed to the helper.
|
|
1075
|
+
*/
|
|
1076
|
+
onPointerDown(e) {
|
|
1077
|
+
try {
|
|
1078
|
+
const tgt = e.target instanceof HTMLElement ? e.target : null;
|
|
1079
|
+
if (tgt?.closest('[role="combobox"], [data-docs-navigation]'))
|
|
1080
|
+
return;
|
|
1081
|
+
snapshotSelection();
|
|
1082
|
+
if (isToolbarTgt(tgt)) {
|
|
1083
|
+
restoreSavedSelection('pointerdown-toolbar');
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const block = tgt?.closest('[data-block-gutter="true"]')?.parentElement?.querySelector('[data-docs-editor-block], [data-editor-block], .docs-editor-block') ??
|
|
1087
|
+
tgt?.closest('[data-docs-editor-block], [data-editor-block]');
|
|
1088
|
+
const dec = block?.closest('[data-lexical-decorator="true"]') ?? tgt?.closest('[data-lexical-decorator="true"]') ?? block;
|
|
1089
|
+
if (!dec || !refs.current?.body?.contains(dec)) {
|
|
1090
|
+
clear();
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const eTgt = tgt?.closest(EDITABLE);
|
|
1094
|
+
const iTgt = tgt?.closest(INTERACTIVE);
|
|
1095
|
+
const wrap = dec.matches('[data-docs-editor-block]') ? dec : (dec.querySelector('[data-docs-editor-block]') ?? dec);
|
|
1096
|
+
const wrapState = {
|
|
1097
|
+
box: wrap.getBoundingClientRect(),
|
|
1098
|
+
sameBlock: bCursor.current?.wrapper === wrap ||
|
|
1099
|
+
(bCursor.current?.label === wrap.getAttribute('data-docs-editor-block') &&
|
|
1100
|
+
bCursor.current?.gId ===
|
|
1101
|
+
(wrap.matches('[data-docs-focus-group-id]') ? wrap : wrap.querySelector('[data-docs-focus-group-id]'))?.getAttribute('data-docs-focus-group-id')),
|
|
1102
|
+
};
|
|
1103
|
+
if ((eTgt && dec.contains(eTgt)) || (iTgt && dec.contains(iTgt))) {
|
|
1104
|
+
if (bCursor.current)
|
|
1105
|
+
clear();
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const gId = tgt?.closest('[data-docs-focus-group-id]')?.getAttribute('data-docs-focus-group-id') ?? null;
|
|
1109
|
+
if (gId && bCursor.current?.gId !== gId) {
|
|
1110
|
+
const gRoot = root()?.querySelector(`[data-docs-focus-group="root"][data-docs-focus-group-id="${gId}"]`);
|
|
1111
|
+
const gDec = gRoot?.closest('[data-lexical-decorator="true"]') ?? dec;
|
|
1112
|
+
const gBox = (gRoot ?? gDec).getBoundingClientRect();
|
|
1113
|
+
e.preventDefault();
|
|
1114
|
+
e.stopPropagation();
|
|
1115
|
+
focus(gDec, e.clientX - gBox.left > gBox.width / 2 ? 'right' : 'left');
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
e.preventDefault();
|
|
1119
|
+
e.stopPropagation();
|
|
1120
|
+
if (wrapState.sameBlock)
|
|
1121
|
+
return;
|
|
1122
|
+
focus(dec, e.clientX - wrapState.box.left > wrapState.box.width / 2 ? 'right' : 'left');
|
|
1123
|
+
}
|
|
1124
|
+
catch (err) {
|
|
1125
|
+
console.error('[docs:edit] Pointer down event failed', err);
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
/**
|
|
1129
|
+
* Selects a decorator block from its surrounding interactive ring.
|
|
1130
|
+
*
|
|
1131
|
+
* @param e Ring selection event emitted by the visual block wrapper.
|
|
1132
|
+
*
|
|
1133
|
+
* @returns Completion after the decorator becomes the active block selection.
|
|
1134
|
+
*/
|
|
1135
|
+
onBlockSelect(e) {
|
|
1136
|
+
const decorator = e.detail?.decorator;
|
|
1137
|
+
if (!(decorator instanceof HTMLElement) || !refs.current.body?.contains(decorator))
|
|
1138
|
+
return;
|
|
1139
|
+
focus(decorator, decorator.getBoundingClientRect().width > 0 ? 'left' : 'right');
|
|
1140
|
+
const block = decorator.querySelector('[data-docs-editor-block], .docs-math-wrapper, [data-math-code], [data-docs-view-root="true"]');
|
|
1141
|
+
if (block)
|
|
1142
|
+
document.dispatchEvent(new CustomEvent('docs:block-selected', { detail: { block } }));
|
|
1143
|
+
},
|
|
1144
|
+
};
|
|
1145
|
+
document.addEventListener('keydown', eHandlers.onKeyDown, true);
|
|
1146
|
+
document.addEventListener('selectionchange', eHandlers.onSelectionChange);
|
|
1147
|
+
document.addEventListener('pointerdown', eHandlers.onPointerDown, true);
|
|
1148
|
+
document.addEventListener('docs:block-select', eHandlers.onBlockSelect);
|
|
1149
|
+
return () => {
|
|
1150
|
+
window.cancelAnimationFrame(restoreCursor.current);
|
|
1151
|
+
window.cancelAnimationFrame(vCursor.current);
|
|
1152
|
+
vPending.current = null;
|
|
1153
|
+
clear();
|
|
1154
|
+
document.removeEventListener('keydown', eHandlers.onKeyDown, true);
|
|
1155
|
+
document.removeEventListener('selectionchange', eHandlers.onSelectionChange);
|
|
1156
|
+
document.removeEventListener('pointerdown', eHandlers.onPointerDown, true);
|
|
1157
|
+
document.removeEventListener('docs:block-select', eHandlers.onBlockSelect);
|
|
1158
|
+
};
|
|
1159
|
+
}, [refs]);
|
|
1160
|
+
},
|
|
1161
|
+
/**
|
|
1162
|
+
* Manages dragging, dropping, and clipboard paste events for media assets.
|
|
1163
|
+
*
|
|
1164
|
+
* @param refs - Refs input supplied to use media paste's operation.
|
|
1165
|
+
* @param insert - Insert input supplied to use media paste's operation.
|
|
1166
|
+
* @param uploadFile - Upload file input supplied to use media paste's operation.
|
|
1167
|
+
* @param attachFiles - Attach files input supplied to use media paste's operation.
|
|
1168
|
+
*
|
|
1169
|
+
* @return Effects that register and clean up the editor's media paste and drag-and-drop handlers.
|
|
1170
|
+
*/
|
|
1171
|
+
useMediaPaste(refs, insert, uploadFile, attachFiles) {
|
|
1172
|
+
useEffect(() => {
|
|
1173
|
+
if (!document)
|
|
1174
|
+
return;
|
|
1175
|
+
// DROP
|
|
1176
|
+
/**
|
|
1177
|
+
* Moves the move Drop Caret for the docs.
|
|
1178
|
+
*
|
|
1179
|
+
* @param e - Value passed to the helper.
|
|
1180
|
+
*/
|
|
1181
|
+
const moveDropCaret = (e) => {
|
|
1182
|
+
try {
|
|
1183
|
+
const root = refs.current?.body;
|
|
1184
|
+
const transfer = e.dataTransfer;
|
|
1185
|
+
if (!transfer || !root)
|
|
1186
|
+
return;
|
|
1187
|
+
if (!['Files', 'text/html', 'text/plain', 'text/uri-list'].some(t => Array.from(transfer.types ?? []).includes(t)))
|
|
1188
|
+
return;
|
|
1189
|
+
const range = document.caretRangeFromPoint?.(e.clientX, e.clientY) ??
|
|
1190
|
+
(() => {
|
|
1191
|
+
const pos = document.caretPositionFromPoint?.(e.clientX, e.clientY);
|
|
1192
|
+
if (!pos)
|
|
1193
|
+
return null;
|
|
1194
|
+
const n = document.createRange();
|
|
1195
|
+
n.setStart(pos.offsetNode, pos.offset);
|
|
1196
|
+
n.collapse(true);
|
|
1197
|
+
return n;
|
|
1198
|
+
})();
|
|
1199
|
+
if (!range || !root.contains(range.startContainer))
|
|
1200
|
+
return;
|
|
1201
|
+
const sel = window.getSelection();
|
|
1202
|
+
if (!sel)
|
|
1203
|
+
return;
|
|
1204
|
+
sel.removeAllRanges();
|
|
1205
|
+
sel.addRange(range);
|
|
1206
|
+
e.preventDefault();
|
|
1207
|
+
}
|
|
1208
|
+
catch (err) {
|
|
1209
|
+
console.error('[docs:edit] Drop event failed', err);
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
/**
|
|
1213
|
+
* Handles on drop.
|
|
1214
|
+
*
|
|
1215
|
+
* @param e - Editor event carrying the changed Docs content.
|
|
1216
|
+
*/
|
|
1217
|
+
const onDrop = (e) => {
|
|
1218
|
+
try {
|
|
1219
|
+
const dropped = Array.from(e.dataTransfer?.files ?? []);
|
|
1220
|
+
if (!dropped.length || !refs.current.body)
|
|
1221
|
+
return;
|
|
1222
|
+
e.preventDefault();
|
|
1223
|
+
e.stopPropagation();
|
|
1224
|
+
const files = dropped.filter(f => {
|
|
1225
|
+
const ext = f.name.match(TerraQualityRegex.REGEX_PATTERN_REGEX())?.[0].toLowerCase();
|
|
1226
|
+
return Boolean((f.type && f.type in MIME) || (ext && Object.values(MIME).includes(ext)));
|
|
1227
|
+
});
|
|
1228
|
+
if (!files.length) {
|
|
1229
|
+
notify({ error: new Error('Dropped files are not permitted attachments.'), fallback: 'Only permitted file types can be attached.' });
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
void attachFiles(files).catch((e) => {
|
|
1233
|
+
notify({ error: e instanceof Error ? e : new Error(String(e)), fallback: 'Failed to add dropped attachments.' });
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
console.error('[docs:edit] Attachment drop failed', err);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
// PASTE
|
|
1241
|
+
/**
|
|
1242
|
+
* Handles clipboard.
|
|
1243
|
+
*
|
|
1244
|
+
* @param data - Source data to transform.
|
|
1245
|
+
* @param e - Value passed to the helper.
|
|
1246
|
+
*/
|
|
1247
|
+
const handleClipboard = (data, e) => {
|
|
1248
|
+
const view = codeViewOf();
|
|
1249
|
+
const sel = window.getSelection();
|
|
1250
|
+
const savedSel = refs.current.selection ?? $cursorPosition.get() ?? $cursor.get() ?? $cursorGhost.get();
|
|
1251
|
+
if (!refs.current.selection && view && view.dom.offsetParent !== null) {
|
|
1252
|
+
refs.current.selection = { code: { from: view.state.selection.main.from, to: view.state.selection.main.to }, collapsed: view.state.selection.main.empty, dom: null, visible: true };
|
|
1253
|
+
$cursorPosition.set({ code: { from: view.state.selection.main.from, to: view.state.selection.main.to }, collapsed: view.state.selection.main.empty, dom: null, visible: true });
|
|
1254
|
+
$cursor.set({ code: { from: view.state.selection.main.from, to: view.state.selection.main.to }, collapsed: view.state.selection.main.empty, dom: null, visible: true });
|
|
1255
|
+
}
|
|
1256
|
+
else if (!refs.current.selection && sel?.rangeCount && sel.anchorNode) {
|
|
1257
|
+
const ce = (sel.anchorNode instanceof Element ? sel.anchorNode : sel.anchorNode.parentElement)?.closest('[contenteditable="true"], [contenteditable="plaintext-only"]');
|
|
1258
|
+
if (ce && refs.current.body?.contains(ce)) {
|
|
1259
|
+
const dom = sel.getRangeAt(0).cloneRange();
|
|
1260
|
+
const rect = dom.getBoundingClientRect();
|
|
1261
|
+
const anchorPath = captureNodePath(refs.current.body, sel.anchorNode);
|
|
1262
|
+
const focusPath = captureNodePath(refs.current.body, sel.focusNode);
|
|
1263
|
+
refs.current.selection = {
|
|
1264
|
+
anchorOffset: sel.anchorOffset,
|
|
1265
|
+
anchorPath,
|
|
1266
|
+
anchorText: sel.anchorNode?.textContent || undefined,
|
|
1267
|
+
box: rect.width || rect.height ? { height: Math.max(16, rect.height || 0), left: rect.left, top: rect.top, width: Math.max(2, rect.width || 0) } : undefined,
|
|
1268
|
+
collapsed: sel.isCollapsed,
|
|
1269
|
+
code: null,
|
|
1270
|
+
dom,
|
|
1271
|
+
focusOffset: sel.focusOffset,
|
|
1272
|
+
focusPath,
|
|
1273
|
+
focusText: sel.focusNode?.textContent || undefined,
|
|
1274
|
+
text: sel.toString(),
|
|
1275
|
+
visible: true,
|
|
1276
|
+
};
|
|
1277
|
+
$cursorPosition.set({
|
|
1278
|
+
anchorOffset: sel.anchorOffset,
|
|
1279
|
+
anchorPath,
|
|
1280
|
+
anchorText: sel.anchorNode?.textContent || undefined,
|
|
1281
|
+
box: rect.width || rect.height ? { height: Math.max(16, rect.height || 0), left: rect.left, top: rect.top, width: Math.max(2, rect.width || 0) } : undefined,
|
|
1282
|
+
collapsed: sel.isCollapsed,
|
|
1283
|
+
code: null,
|
|
1284
|
+
dom,
|
|
1285
|
+
focusOffset: sel.focusOffset,
|
|
1286
|
+
focusPath,
|
|
1287
|
+
focusText: sel.focusNode?.textContent || undefined,
|
|
1288
|
+
text: sel.toString(),
|
|
1289
|
+
visible: true,
|
|
1290
|
+
});
|
|
1291
|
+
$cursor.set({
|
|
1292
|
+
anchorOffset: sel.anchorOffset,
|
|
1293
|
+
anchorPath,
|
|
1294
|
+
anchorText: sel.anchorNode?.textContent || undefined,
|
|
1295
|
+
box: rect.width || rect.height ? { height: Math.max(16, rect.height || 0), left: rect.left, top: rect.top, width: Math.max(2, rect.width || 0) } : undefined,
|
|
1296
|
+
collapsed: sel.isCollapsed,
|
|
1297
|
+
code: null,
|
|
1298
|
+
dom,
|
|
1299
|
+
focusOffset: sel.focusOffset,
|
|
1300
|
+
focusPath,
|
|
1301
|
+
focusText: sel.focusNode?.textContent || undefined,
|
|
1302
|
+
text: sel.toString(),
|
|
1303
|
+
visible: true,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const html = data.getData('text/html');
|
|
1308
|
+
let plain = data.getData('text/plain');
|
|
1309
|
+
const types = Array.from(data.types ?? []);
|
|
1310
|
+
const richClipboard = /<(?:AccordionGroup|AccordionItem|Code|Testimonials|TabsGroup|TabsItem|Leaf|DocsTable|TsDocs|Nodes)\b|<table\b|===|\|\|\||:::/i.test(`${html || ''}\n${plain || ''}`);
|
|
1311
|
+
// CRITICAL BYPASS: Let Lexical handle pasting its own native blocks.
|
|
1312
|
+
if ((types.includes('application/x-lexical-editor') || (html && html.includes('data-lexical-decorator'))) && !richClipboard)
|
|
1313
|
+
return;
|
|
1314
|
+
if (richClipboard) {
|
|
1315
|
+
e.preventDefault();
|
|
1316
|
+
e.stopPropagation();
|
|
1317
|
+
e.stopImmediatePropagation();
|
|
1318
|
+
}
|
|
1319
|
+
// Intercept and sanitize triple character blocks inside raw clipboard copies
|
|
1320
|
+
if (plain && plain.includes('|||'))
|
|
1321
|
+
plain = plain.replace(/([^\n])[ \t]*\|\|\|/g, '$1\n\n|||').replace(/\|\|\|[ \t]*([^\n])/g, '|||\n\n$1');
|
|
1322
|
+
if (richClipboard && plain) {
|
|
1323
|
+
pasteMd(insert, ((md) => {
|
|
1324
|
+
if (!md)
|
|
1325
|
+
return '';
|
|
1326
|
+
return [
|
|
1327
|
+
...new Set([
|
|
1328
|
+
...customComponents.map(component => component.name),
|
|
1329
|
+
'AccordionGroup',
|
|
1330
|
+
'AccordionItem',
|
|
1331
|
+
'Aside',
|
|
1332
|
+
'Code',
|
|
1333
|
+
'DocsMath',
|
|
1334
|
+
'DocsTable',
|
|
1335
|
+
'Embed',
|
|
1336
|
+
'Leaf',
|
|
1337
|
+
'Nodes',
|
|
1338
|
+
'TabsGroup',
|
|
1339
|
+
'TabsItem',
|
|
1340
|
+
'TsDocs',
|
|
1341
|
+
]),
|
|
1342
|
+
]
|
|
1343
|
+
.sort((left, right) => right.length - left.length)
|
|
1344
|
+
.reduce((v, name) => v.replace(new RegExp(`(<\\/?\\s*)${name.replace(TerraQualityRegex.REGEX_G_REGEX_9(), '\\$&')}(?=\\b)`, 'gi'), (_, prefix) => `${prefix}${name}`), md);
|
|
1345
|
+
})(plain), savedSel);
|
|
1346
|
+
if (refs.current && refs.current.norm) {
|
|
1347
|
+
refs.current.norm.force = true;
|
|
1348
|
+
refs.current.norm.replay = true;
|
|
1349
|
+
}
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (html && !types.includes('vscode-editor-data')) {
|
|
1353
|
+
const md = htmlToMd(html);
|
|
1354
|
+
if (md && md !== plain) {
|
|
1355
|
+
pasteMd(insert, md, savedSel);
|
|
1356
|
+
if (refs.current && refs.current.norm) {
|
|
1357
|
+
refs.current.norm.force = true;
|
|
1358
|
+
refs.current.norm.replay = true;
|
|
1359
|
+
}
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (plain) {
|
|
1364
|
+
let transformed = plain;
|
|
1365
|
+
let changed = false;
|
|
1366
|
+
if (/^[ \t]*\\`\\`\\`/gm.test(transformed)) {
|
|
1367
|
+
transformed = transformed.replace(/^([ \t]*)\\`\\`\\`/gm, '$1```');
|
|
1368
|
+
changed = true;
|
|
1369
|
+
}
|
|
1370
|
+
if (transformed.includes('`')) {
|
|
1371
|
+
transformed = transformed.replaceAll('`', '&grave;');
|
|
1372
|
+
changed = true;
|
|
1373
|
+
}
|
|
1374
|
+
const table = mdTableToHtml(transformed);
|
|
1375
|
+
if (table) {
|
|
1376
|
+
transformed = table;
|
|
1377
|
+
changed = true;
|
|
1378
|
+
}
|
|
1379
|
+
if (changed) {
|
|
1380
|
+
pasteMd(insert, transformed, savedSel);
|
|
1381
|
+
if (refs.current && refs.current.norm) {
|
|
1382
|
+
refs.current.norm.force = true;
|
|
1383
|
+
refs.current.norm.replay = true;
|
|
1384
|
+
}
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const file = Array.from(data.files ?? []).find(f => f.type.startsWith('image/') || f.type === 'application/pdf');
|
|
1389
|
+
if (file && file.size > 0) {
|
|
1390
|
+
const isPdf = file.type === 'application/pdf';
|
|
1391
|
+
const optimistic = URL.createObjectURL(file);
|
|
1392
|
+
e.preventDefault();
|
|
1393
|
+
e.stopPropagation();
|
|
1394
|
+
e.stopImmediatePropagation();
|
|
1395
|
+
const lbl = toImgAlt(file.name || (isPdf ? 'document' : 'image'));
|
|
1396
|
+
pasteMd(insert, isPdf ? `\n<Embed src="${optimistic}" title="${lbl}" />\n` : toImgMd(optimistic, lbl), savedSel);
|
|
1397
|
+
void uploadFile(file).catch((e) => {
|
|
1398
|
+
notify({ error: e instanceof Error ? e : new Error(String(e)), fallback: `Failed to upload pasted ${isPdf ? 'PDF' : 'image'}. Local changes were kept.` });
|
|
1399
|
+
});
|
|
1400
|
+
if (refs.current && refs.current.norm) {
|
|
1401
|
+
refs.current.norm.force = true;
|
|
1402
|
+
refs.current.norm.replay = true;
|
|
1403
|
+
}
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const remote = (() => {
|
|
1407
|
+
const isRemote = (val) => {
|
|
1408
|
+
const trimmed = val.trim();
|
|
1409
|
+
if (!trimmed)
|
|
1410
|
+
return false;
|
|
1411
|
+
if (trimmed.startsWith('/api/media?'))
|
|
1412
|
+
return true;
|
|
1413
|
+
if (TerraQualityRegex.REGEX_HTTPS_I_REGEX().test(trimmed)) {
|
|
1414
|
+
try {
|
|
1415
|
+
const imgUrl = new URL(trimmed);
|
|
1416
|
+
if (imgUrl.pathname === '/api/media' || /\.(?:avif|gif|jpe?g|png|svg|webp)(?:$|[?#])/i.test(imgUrl.pathname + imgUrl.search))
|
|
1417
|
+
return true;
|
|
1418
|
+
}
|
|
1419
|
+
catch {
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
try {
|
|
1424
|
+
return (/^https?:$/i.test(new URL(trimmed).protocol) &&
|
|
1425
|
+
new RegExp(`\\.(${Object.values(IMAGE_TYPES)
|
|
1426
|
+
.map(extension => extension.substring(1))
|
|
1427
|
+
.join('|')})(?:$|[?#])`, 'i').test(trimmed));
|
|
1428
|
+
}
|
|
1429
|
+
catch {
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
const html = data.getData('text/html').trim();
|
|
1434
|
+
if (html) {
|
|
1435
|
+
const match = html.match(/<img\b[^>]*\bsrc=(['"])(.*?)\1/i)?.[2]?.trim();
|
|
1436
|
+
if (match && isRemote(match))
|
|
1437
|
+
return match;
|
|
1438
|
+
}
|
|
1439
|
+
for (const type of ['text/uri-list', 'text/plain']) {
|
|
1440
|
+
const val = data.getData(type).trim();
|
|
1441
|
+
if (val && isRemote(val))
|
|
1442
|
+
return val;
|
|
1443
|
+
}
|
|
1444
|
+
return null;
|
|
1445
|
+
})();
|
|
1446
|
+
if (!remote)
|
|
1447
|
+
return;
|
|
1448
|
+
e.preventDefault();
|
|
1449
|
+
e.stopPropagation();
|
|
1450
|
+
e.stopImmediatePropagation();
|
|
1451
|
+
pasteMd(insert, toImgMd(remote, toImgAlt(remote.split('/').at(-1) ?? 'image')), savedSel);
|
|
1452
|
+
if (refs.current && refs.current.norm) {
|
|
1453
|
+
refs.current.norm.force = true;
|
|
1454
|
+
refs.current.norm.replay = true;
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
/**
|
|
1458
|
+
* On paste.
|
|
1459
|
+
*
|
|
1460
|
+
* @param e - Value passed to the helper.
|
|
1461
|
+
*/
|
|
1462
|
+
const onPaste = (e) => {
|
|
1463
|
+
try {
|
|
1464
|
+
if (e.clipboardData)
|
|
1465
|
+
handleClipboard(e.clipboardData, e);
|
|
1466
|
+
}
|
|
1467
|
+
catch (err) {
|
|
1468
|
+
console.error('[docs:edit] Paste event failed', err);
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
/**
|
|
1472
|
+
* On before input.
|
|
1473
|
+
*
|
|
1474
|
+
* @param e - Value passed to the helper.
|
|
1475
|
+
*/
|
|
1476
|
+
const onBeforeInput = (e) => {
|
|
1477
|
+
try {
|
|
1478
|
+
if (e.inputType !== 'insertFromPaste' && e.inputType !== 'insertReplacementText')
|
|
1479
|
+
return;
|
|
1480
|
+
if (!e.dataTransfer)
|
|
1481
|
+
return;
|
|
1482
|
+
handleClipboard(e.dataTransfer, e);
|
|
1483
|
+
}
|
|
1484
|
+
catch (err) {
|
|
1485
|
+
console.error('[docs:edit] Beforeinput paste event failed', err);
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
document.addEventListener('dragover', moveDropCaret, true);
|
|
1489
|
+
document.addEventListener('drop', onDrop, true);
|
|
1490
|
+
document.addEventListener('paste', onPaste, true);
|
|
1491
|
+
document.addEventListener('beforeinput', onBeforeInput, true);
|
|
1492
|
+
return () => {
|
|
1493
|
+
document.removeEventListener('dragover', moveDropCaret, true);
|
|
1494
|
+
document.removeEventListener('drop', onDrop, true);
|
|
1495
|
+
document.removeEventListener('paste', onPaste, true);
|
|
1496
|
+
document.removeEventListener('beforeinput', onBeforeInput, true);
|
|
1497
|
+
};
|
|
1498
|
+
}, [attachFiles, insert, refs, uploadFile]);
|
|
1499
|
+
},
|
|
1500
|
+
};
|
|
1501
|
+
export const useBlockNav = EditorHooks.useBlockNav;
|
|
1502
|
+
export const useMediaPaste = EditorHooks.useMediaPaste;
|
|
1503
|
+
/**
|
|
1504
|
+
* Renders the view component.
|
|
1505
|
+
*
|
|
1506
|
+
* @param props - Document body and optional reset key used to render the read-only Docs editor surface.
|
|
1507
|
+
* @param body - Body input supplied to view's operation.
|
|
1508
|
+
* @param resetKey - Key used to reset Terra's editor state for a new document.
|
|
1509
|
+
*
|
|
1510
|
+
* @return The read-only editor view for the supplied document body.
|
|
1511
|
+
*/
|
|
1512
|
+
function View({ body, resetKey }) {
|
|
1513
|
+
useEffect(() => {
|
|
1514
|
+
return ((timer) => () => {
|
|
1515
|
+
window.clearTimeout(timer);
|
|
1516
|
+
if ($surfaceReady.get() === 'view')
|
|
1517
|
+
$surfaceReady.set(null);
|
|
1518
|
+
})(window.setTimeout(() => {
|
|
1519
|
+
$surfaceReady.set('view');
|
|
1520
|
+
document.dispatchEvent(new Event('docs:surface-ready'));
|
|
1521
|
+
}));
|
|
1522
|
+
}, []);
|
|
1523
|
+
return _jsx(TerraContentEditor, { content: body, docsAttachments: [], memberProfile: 'docs', readOnly: true, ...(resetKey !== undefined && { resetKey }) });
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Reads the editor surface context when a Docs editor provider is present.
|
|
1527
|
+
*
|
|
1528
|
+
* @return The active editor surface, or `null` when this component is outside an editor provider.
|
|
1529
|
+
*/
|
|
1530
|
+
function useEditorSurfaceOptional() {
|
|
1531
|
+
return useContext(EditorSurfaceCtx);
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Wraps lexical engine maintaining layout constraints.
|
|
1535
|
+
*
|
|
1536
|
+
* @param props - Optional wrapper classes for the editable Lexical editor surface.
|
|
1537
|
+
* @param className - Additional CSS classes merged into edit.
|
|
1538
|
+
*
|
|
1539
|
+
* @return The interactive Docs editing surface with its configured toolbar.
|
|
1540
|
+
*/
|
|
1541
|
+
const EditorSurfaces = {
|
|
1542
|
+
Edit({ className }) {
|
|
1543
|
+
// INIT
|
|
1544
|
+
const surface = useEditorSurfaceOptional();
|
|
1545
|
+
const editorRefs = { toolbar: useAtom($toolbar), empty: useRef({}) };
|
|
1546
|
+
const [init, setInit] = useState(false);
|
|
1547
|
+
const transition = useContext(TransitionCtx);
|
|
1548
|
+
useLayoutEffect(() => {
|
|
1549
|
+
if (!init)
|
|
1550
|
+
transition?.setLoading(true);
|
|
1551
|
+
}, [init, transition]);
|
|
1552
|
+
useLayoutEffect(() => {
|
|
1553
|
+
$editReadyPath.set(null);
|
|
1554
|
+
$surfaceReady.set(null);
|
|
1555
|
+
setInit(false);
|
|
1556
|
+
if (!surface)
|
|
1557
|
+
return;
|
|
1558
|
+
if (!surface.editor.loaded)
|
|
1559
|
+
return;
|
|
1560
|
+
resetEditorMounts(surface.path, ['root']);
|
|
1561
|
+
if (surface.parseError) {
|
|
1562
|
+
setInit(true);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const body = surface.refs.current.body;
|
|
1566
|
+
if (!body)
|
|
1567
|
+
return;
|
|
1568
|
+
let frame = 0;
|
|
1569
|
+
const mountObserver = {
|
|
1570
|
+
check() {
|
|
1571
|
+
const root = body.querySelector('[data-terra-content-editor]');
|
|
1572
|
+
if (root) {
|
|
1573
|
+
setEditorMount(surface.path, 'root', true);
|
|
1574
|
+
observer.disconnect();
|
|
1575
|
+
}
|
|
1576
|
+
const mounts = $editorMounts.get();
|
|
1577
|
+
setInit(!!root && mounts.path === surface.path && mounts.expected.length === mounts.mounted.length);
|
|
1578
|
+
},
|
|
1579
|
+
queue() {
|
|
1580
|
+
window.cancelAnimationFrame(frame);
|
|
1581
|
+
frame = window.requestAnimationFrame(mountObserver.check);
|
|
1582
|
+
},
|
|
1583
|
+
};
|
|
1584
|
+
const observer = new MutationObserver(mountObserver.queue);
|
|
1585
|
+
observer.observe(body, { childList: true, subtree: true });
|
|
1586
|
+
mountObserver.queue();
|
|
1587
|
+
return () => {
|
|
1588
|
+
window.cancelAnimationFrame(frame);
|
|
1589
|
+
setEditorMount(surface.path, 'root', false);
|
|
1590
|
+
observer.disconnect();
|
|
1591
|
+
};
|
|
1592
|
+
}, [surface?.path, surface?.parseError, surface?.editor.loaded]);
|
|
1593
|
+
useEffect(() => {
|
|
1594
|
+
$editReadyPath.set(null);
|
|
1595
|
+
$surfaceReady.set(null);
|
|
1596
|
+
if (!init)
|
|
1597
|
+
return;
|
|
1598
|
+
return ((timer) => () => {
|
|
1599
|
+
window.clearTimeout(timer);
|
|
1600
|
+
if ($editReadyPath.get() === surface?.path)
|
|
1601
|
+
$editReadyPath.set(null);
|
|
1602
|
+
if ($surfaceReady.get() === 'edit')
|
|
1603
|
+
$surfaceReady.set(null);
|
|
1604
|
+
})(window.setTimeout(() => {
|
|
1605
|
+
$editReadyPath.set(surface?.path || null);
|
|
1606
|
+
$surfaceReady.set('edit');
|
|
1607
|
+
document.dispatchEvent(new Event('docs:surface-ready'));
|
|
1608
|
+
}, 50));
|
|
1609
|
+
}, [init, surface?.path]);
|
|
1610
|
+
// bind keyboard shortcuts and rich media ingestion at the root
|
|
1611
|
+
// these must execute unconditionally to satisfy React hooks rules
|
|
1612
|
+
EditorHooks.useBlockNav(surface ? surface.refs : editorRefs.empty);
|
|
1613
|
+
EditorHooks.useMediaPaste(surface ? surface.refs : editorRefs.empty, surface?.insert ?? (() => undefined), surface?.uploadImage ?? (async () => ''), surface?.attachFiles ?? (async () => undefined));
|
|
1614
|
+
// VALIDATION
|
|
1615
|
+
if (!surface)
|
|
1616
|
+
return _jsx(AlertError, { title: 'Editor failed to initialize.', description: 'Reload the page to reattach the docs editor provider.' });
|
|
1617
|
+
const { editor, parseError: parseErr, refs } = surface;
|
|
1618
|
+
// RENDER
|
|
1619
|
+
return (_jsxs("div", { ref: e => {
|
|
1620
|
+
refs.current.body = e;
|
|
1621
|
+
}, className: cn('min-w-0', className, '[&_[class*="_toolbarRoot_"]]:static [&_[class*="_toolbarRoot_"]]:!top-auto [&_[class*="_toolbarRoot_"]]:!z-auto [&_[class*="_toolbarRoot_"]]:m-0 [&_[class*="_toolbarRoot_"]]:w-full [&_[class*="_toolbarRoot_"]]:gap-0 [&_[class*="_toolbarRoot_"]]:overflow-visible [&_[class*="_toolbarRoot_"]]:rounded-none [&_[class*="_toolbarRoot_"]]:border-none [&_[class*="_toolbarRoot_"]]:!bg-transparent [&_[class*="_toolbarRoot_"]]:p-0 [&_[class*="_toolbarRoot_"]]:!shadow-none [&_[class*="_toolbarRoot_"]]:outline-none [&_[class*="_toolbarRoot_"]]:ring-0 focus-within:[&_[class*="_toolbarRoot_"]]:!shadow-none'), "data-docs-body-editor": true, "data-docs-editor": 'inline', "data-docs-editor-initialized": init, "data-docs-path": editor.draft.path, children: [editor.busy === 'loading' && _jsx("div", { "aria-hidden": 'true', "data-docs-editor-loading": true }), parseErr ? (_jsxs(_Fragment, { children: [_jsx(AlertError, { description: `${parseErr}\n\nThe source remains editable below.`, title: 'Visual editor fallback' }), _jsx("textarea", { className: 'mt-4 min-h-[32rem] w-full rounded-md border border-primary-6 bg-primary-1 p-4 font-mono text-sm leading-relaxed outline-none focus:ring-2 focus:ring-primary-8', value: editor.draft.source || '', onChange: e => surface.onMdChange(e.currentTarget.value), "aria-label": 'Document source fallback' })] })) : (_jsx(Suspense, { fallback: _jsx("div", { "aria-hidden": 'true', "data-docs-editor-loading": true }), children: _jsx(TerraContentEditor, { content: (isDocBody(editor.draft.content) ? editor.draft.content : undefined), docsAttachments: [], memberProfile: 'docs', onChange: c => {
|
|
1622
|
+
surface.onBodyChange(toPortableBody(c.map(b => (b._type === 'terraBlock' ? { ...b, _type: 'block' } : b))));
|
|
1623
|
+
}, resetKey: `${editor.draft.path}:${editor.version}` }) }))] }));
|
|
1624
|
+
},
|
|
1625
|
+
/**
|
|
1626
|
+
* Renders the head component.
|
|
1627
|
+
*
|
|
1628
|
+
* @param props - Compact layout, editability, page metadata, contributors, and timestamps displayed in the Docs page header.
|
|
1629
|
+
* @param full - Whether the page is rendered in full mode.
|
|
1630
|
+
* @param contributors - Contributors input supplied to head's operation.
|
|
1631
|
+
* @param updatedAt - Updated at input supplied to head's operation.
|
|
1632
|
+
* @param editable - Editability flag controlling whether head accepts changes.
|
|
1633
|
+
* @param description - Supporting text shown in head.
|
|
1634
|
+
* @param title - Heading text shown in head.
|
|
1635
|
+
*
|
|
1636
|
+
* @return The page head block.
|
|
1637
|
+
*
|
|
1638
|
+
* @remarks Renders the docs page head metadata and edit controls.
|
|
1639
|
+
*/
|
|
1640
|
+
Head({ compact = false, description = '', editable, title, full, contributors: initContributors = [], updatedAt, }) {
|
|
1641
|
+
// STATES
|
|
1642
|
+
const edit = useContext(EditStateCtx);
|
|
1643
|
+
const draft = edit?.draft ?? { description, title };
|
|
1644
|
+
const contributors = initContributors ?? [];
|
|
1645
|
+
// RENDER
|
|
1646
|
+
// prettier-ignore
|
|
1647
|
+
return (_jsx("header", { className: 'terra-docs-document-header relative z-10 w-full print:!mb-8 print:border-none', children: _jsxs("div", { className: cn('mx-auto w-full px-6 transition-opacity duration-200 md:px-10 print:!pb-3', compact ? 'pb-3 pt-5' : 'pb-10 pt-7', full ? 'max-w-none' : 'max-w-5xl'), children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: 'docs-title', className: 'sr-only', children: "Title" }), _jsx(Input, { id: 'docs-title', value: draft.title, onChange: e => {
|
|
1648
|
+
const title = e.currentTarget.value;
|
|
1649
|
+
edit?.update({ title });
|
|
1650
|
+
if (!edit?.draft.path)
|
|
1651
|
+
return;
|
|
1652
|
+
$leafTitle.set({ path: edit.draft.path, title });
|
|
1653
|
+
setDocumentTitle(title);
|
|
1654
|
+
}, placeholder: 'Page title', disabled: !editable, className: 'mb-2 mt-0 h-fit border-none bg-transparent p-0 font-serif italic tracking-tight text-4xl text-primary-12 shadow-none focus-visible:ring-0 md:text-5xl disabled:cursor-default disabled:opacity-100 dark:bg-transparent', suppressHydrationWarning: true })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: 'docs-description', className: 'sr-only', children: "Description" }), _jsx(Textarea, { id: 'docs-description', value: draft.description, onChange: e => edit?.update({ description: e.currentTarget.value }), placeholder: 'Add a description...', disabled: !editable, className: 'min-h-0 resize-none border-none bg-transparent p-0 text-base text-pretty text-primary-11 shadow-none focus-visible:ring-0 disabled:cursor-default disabled:opacity-100 dark:bg-transparent', suppressHydrationWarning: true })] }), (updatedAt || contributors.length > 0) && (_jsxs("div", { className: 'mt-4 flex flex-wrap items-center gap-2 text-sm text-primary-11', children: [updatedAt && _jsxs("span", { children: ["Last modified ", new Intl.DateTimeFormat('en-AU', { day: 'numeric', month: 'short', year: 'numeric' }).format(new Date(updatedAt))] }), contributors.map(c => _jsx(Badge, { size: 'sm', title: 'Contributing author', variant: 'secondary', children: c }, c))] }))] }) }));
|
|
1655
|
+
},
|
|
1656
|
+
};
|
|
1657
|
+
/**
|
|
1658
|
+
* Renders the scaffold component.
|
|
1659
|
+
*
|
|
1660
|
+
* @param props - Tree, route, user, document content, edit permission, metadata, and display mode for the Docs page shell.
|
|
1661
|
+
* @param tree - Tree input supplied to scaffold's operation.
|
|
1662
|
+
* @param pathname - Pathname input supplied to scaffold's operation.
|
|
1663
|
+
* @param permission - Permission input supplied to scaffold's operation.
|
|
1664
|
+
* @param userData - user record supplied to scaffold.
|
|
1665
|
+
* @param description - Supporting text shown in scaffold.
|
|
1666
|
+
* @param contributors - Contributors input supplied to scaffold's operation.
|
|
1667
|
+
* @param updatedAt - Updated at input supplied to scaffold's operation.
|
|
1668
|
+
* @param full - Full input supplied to scaffold's operation.
|
|
1669
|
+
* @param standalone - Standalone input supplied to scaffold's operation.
|
|
1670
|
+
* @param path - Source or route path resolved for scaffold.
|
|
1671
|
+
* @param compiledMDX - Compiled mdx input supplied to scaffold's operation.
|
|
1672
|
+
* @param title - Heading text shown in scaffold.
|
|
1673
|
+
* @param source - Source node identifier for the connection drawn by scaffold.
|
|
1674
|
+
* @param content - Content input supplied to scaffold's operation.
|
|
1675
|
+
* @param mode - Mode input supplied to scaffold's operation.
|
|
1676
|
+
*
|
|
1677
|
+
* @return The Docs scaffold with its navigation tree, content surface, and page controls.
|
|
1678
|
+
*/
|
|
1679
|
+
export default function Scaffold({ tree, pathname: initPathname, permission, userData, description: initDesc, contributors: initContributors, updatedAt: initUpdatedAt, full = false, standalone = false, path: initPath, compiledMDX: initMdx = '', title: initTitle, source: initSrc, content: initContent, mode: initialMode, }) {
|
|
1680
|
+
// STATES
|
|
1681
|
+
const navigationState = { open: useAtom($sidebar), leafTitle: useAtom($leafTitle) };
|
|
1682
|
+
const [hydrated, setHydrated] = useState(standalone);
|
|
1683
|
+
const toolbar = useAtom($toolbar);
|
|
1684
|
+
const stored = useAtom($mode);
|
|
1685
|
+
const writable = permission === 'write';
|
|
1686
|
+
const modeSynced = useRef(false);
|
|
1687
|
+
const pendingMode = useRef(initialMode ?? null);
|
|
1688
|
+
const contributors = useMemo(() => initContributors ?? [], [initContributors]);
|
|
1689
|
+
// prettier-ignore
|
|
1690
|
+
const [dest, setDest] = useState({ open: false, section: '', group: '', title: 'Untitled', newSection: false, });
|
|
1691
|
+
const destRes = useRef(null);
|
|
1692
|
+
const sects = useMemo(() => tree.filter((n) => n.type === 'branch'), [tree]);
|
|
1693
|
+
const destActions = {
|
|
1694
|
+
groups: useMemo(() => {
|
|
1695
|
+
if (!dest.section)
|
|
1696
|
+
return [];
|
|
1697
|
+
return sects.find(s => s.origName === dest.section)?.children?.filter((c) => c.type === 'branch') ?? [];
|
|
1698
|
+
}, [sects, dest.section]),
|
|
1699
|
+
finish: useCallback(async () => {
|
|
1700
|
+
if (!dest.section || !dest.group)
|
|
1701
|
+
return;
|
|
1702
|
+
const section = dest.newSection ? dest.section.trim().toLowerCase().replace(TerraQualityRegex.REGEX_A_Z0_9_G_REGEX(), '-') : dest.section;
|
|
1703
|
+
const group = dest.newSection ? dest.group.trim().toLowerCase().replace(TerraQualityRegex.REGEX_A_Z0_9_G_REGEX(), '-') : dest.group;
|
|
1704
|
+
const sel = sects.find(i => i.origName === section)?.children?.find(i => i.type === 'branch' && i.origName === group);
|
|
1705
|
+
const selPath = sel?.type === 'branch' ? sel.children?.find(i => i.type === 'leaf' && Boolean(i.path)) : undefined;
|
|
1706
|
+
const leafPath = selPath?.type === 'leaf' ? selPath.path : undefined;
|
|
1707
|
+
const selBase = leafPath ? dirname(leafPath) : '';
|
|
1708
|
+
const destPath = {
|
|
1709
|
+
base: !dest.newSection && selBase && selBase !== '.' ? selBase : `${section}/${group}`,
|
|
1710
|
+
slug: dest.title.trim().toLowerCase().replace(TerraQualityRegex.REGEX_A_Z0_9_G_REGEX(), '-').replace(TerraQualityRegex.REGEX_G_REGEX_20(), '') || 'untitled',
|
|
1711
|
+
};
|
|
1712
|
+
destRes.current?.({ path: `${destPath.base}/${destPath.slug}.mdx`, parentId: dest.newSection ? undefined : sel?.origName, title: dest.title.trim() || 'Untitled' });
|
|
1713
|
+
destRes.current = null;
|
|
1714
|
+
setDest(p => ({ ...p, open: false }));
|
|
1715
|
+
}, [dest, sects]),
|
|
1716
|
+
};
|
|
1717
|
+
// prettier-ignore
|
|
1718
|
+
const [currDoc, setCurrDoc] = useState({ pathname: initPathname, path: initPath, title: initTitle, description: initDesc, contributors, updatedAt: initUpdatedAt, source: initSrc || (typeof initContent === 'string' ? initContent : ''), content: toPortableBody(initContent), compiledMDX: initMdx, versions: [] });
|
|
1719
|
+
// 1. ASTRO VIEW TRANSITION / HMR PROP SYNC
|
|
1720
|
+
// Force component state to update if Astro swaps the underlying page and injects new props into this instance.
|
|
1721
|
+
useEffect(() => {
|
|
1722
|
+
const content = toPortableBody(initContent);
|
|
1723
|
+
const src = initSrc || (typeof initContent === 'string' ? initContent : '');
|
|
1724
|
+
setCurrDoc(p => {
|
|
1725
|
+
if (p.pathname === initPathname &&
|
|
1726
|
+
p.path === initPath &&
|
|
1727
|
+
p.title === initTitle &&
|
|
1728
|
+
p.description === initDesc &&
|
|
1729
|
+
p.updatedAt === initUpdatedAt &&
|
|
1730
|
+
p.source === src &&
|
|
1731
|
+
p.compiledMDX === initMdx &&
|
|
1732
|
+
p.contributors.length === contributors.length &&
|
|
1733
|
+
p.contributors.every((c, i) => c === contributors[i]) &&
|
|
1734
|
+
JSON.stringify(p.content) === JSON.stringify(content))
|
|
1735
|
+
return p;
|
|
1736
|
+
return {
|
|
1737
|
+
...p,
|
|
1738
|
+
pathname: initPathname,
|
|
1739
|
+
path: initPath,
|
|
1740
|
+
title: initTitle,
|
|
1741
|
+
description: initDesc,
|
|
1742
|
+
contributors,
|
|
1743
|
+
updatedAt: initUpdatedAt,
|
|
1744
|
+
source: src,
|
|
1745
|
+
content,
|
|
1746
|
+
compiledMDX: initMdx,
|
|
1747
|
+
versions: p.versions,
|
|
1748
|
+
};
|
|
1749
|
+
});
|
|
1750
|
+
}, [initPathname, initPath, initTitle, initDesc, contributors, initUpdatedAt, initSrc, initContent, initMdx]);
|
|
1751
|
+
useEffect(() => {
|
|
1752
|
+
/**
|
|
1753
|
+
* Handles rename.
|
|
1754
|
+
*
|
|
1755
|
+
* @param e - Pointer event received from the editor control.
|
|
1756
|
+
*/
|
|
1757
|
+
const rename = (e) => {
|
|
1758
|
+
if (!e.detail.isActive || typeof e.detail.title !== 'string')
|
|
1759
|
+
return;
|
|
1760
|
+
setCurrDoc(p => ({ ...p, title: e.detail.title }));
|
|
1761
|
+
};
|
|
1762
|
+
document.addEventListener('docs:rename', rename);
|
|
1763
|
+
return () => {
|
|
1764
|
+
document.removeEventListener('docs:rename', rename);
|
|
1765
|
+
};
|
|
1766
|
+
}, []);
|
|
1767
|
+
useEffect(() => {
|
|
1768
|
+
if (currDoc.path)
|
|
1769
|
+
$leafTitle.set({ path: currDoc.path, title: currDoc.title });
|
|
1770
|
+
}, [currDoc.path, currDoc.title]);
|
|
1771
|
+
useEffect(() => {
|
|
1772
|
+
setDocumentTitle(navigationState.leafTitle?.path === currDoc.path ? (navigationState.leafTitle?.title ?? currDoc.title) : currDoc.title);
|
|
1773
|
+
}, [currDoc.path, currDoc.title, navigationState.leafTitle]);
|
|
1774
|
+
useEffect(() => {
|
|
1775
|
+
/**
|
|
1776
|
+
* Handles update favicon.
|
|
1777
|
+
*/
|
|
1778
|
+
const updateFavicon = () => {
|
|
1779
|
+
const favicon = document.getElementById('favicon');
|
|
1780
|
+
if (!favicon)
|
|
1781
|
+
return;
|
|
1782
|
+
const href = favicon.href.split(',')[1];
|
|
1783
|
+
if (!href)
|
|
1784
|
+
return;
|
|
1785
|
+
const styles = getComputedStyle(document.documentElement);
|
|
1786
|
+
const primary = styles.getPropertyValue('--primaryLight9').trim();
|
|
1787
|
+
const secondary = styles.getPropertyValue('--secondaryLight5').trim();
|
|
1788
|
+
if (!primary || !secondary)
|
|
1789
|
+
return;
|
|
1790
|
+
favicon.href = `data:image/svg+xml,${encodeURIComponent(decodeURIComponent(href)
|
|
1791
|
+
.replace(/fill="[^"]*"\s*stroke="[^"]*"/, `fill="rgb(${secondary})" stroke="rgb(${secondary})"`)
|
|
1792
|
+
.replace(/fill="[^"]*"/, `fill="rgb(${primary})"`))}`;
|
|
1793
|
+
};
|
|
1794
|
+
updateFavicon();
|
|
1795
|
+
document.addEventListener('docs:hue-change', updateFavicon);
|
|
1796
|
+
return () => {
|
|
1797
|
+
document.removeEventListener('docs:hue-change', updateFavicon);
|
|
1798
|
+
};
|
|
1799
|
+
}, []);
|
|
1800
|
+
// 2. SPA NAVIGATION INTERCEPT & URL SYNC
|
|
1801
|
+
// Only updates the state, letting React naturally handle side effects.
|
|
1802
|
+
useEffect(() => {
|
|
1803
|
+
/**
|
|
1804
|
+
* Handles handle nav.
|
|
1805
|
+
*
|
|
1806
|
+
* @param e - Keyboard or pointer event received from the editor control.
|
|
1807
|
+
*/
|
|
1808
|
+
const handleNav = (e) => {
|
|
1809
|
+
setCurrDoc(previous => {
|
|
1810
|
+
const detail = (e.detail || {});
|
|
1811
|
+
const rawPathname = typeof detail.pathname === 'string' ? detail.pathname : typeof detail.url === 'string' ? detail.url : previous.pathname || '';
|
|
1812
|
+
return {
|
|
1813
|
+
...previous,
|
|
1814
|
+
...detail,
|
|
1815
|
+
source: Object.prototype.hasOwnProperty.call(detail, 'source') && typeof detail.source === 'string' ? detail.source : previous.source || '',
|
|
1816
|
+
content: isDocBody(detail.content) ? detail.content : previous.content,
|
|
1817
|
+
pathname: keepDeepPath(previous.pathname || '', rawPathname ? rawPathname.split('?')[0] || '' : previous.pathname || ''),
|
|
1818
|
+
compiledMDX: Object.prototype.hasOwnProperty.call(detail, 'compiledMDX') && detail.compiledMDX !== undefined ? String(detail.compiledMDX) : (previous.compiledMDX ?? ''),
|
|
1819
|
+
versions: previous.versions,
|
|
1820
|
+
};
|
|
1821
|
+
});
|
|
1822
|
+
};
|
|
1823
|
+
document.addEventListener('docs:navigate', handleNav);
|
|
1824
|
+
window.addEventListener('docs:navigate', handleNav);
|
|
1825
|
+
// Handle 403/404 Soft-Redirect Intercepts
|
|
1826
|
+
/**
|
|
1827
|
+
* Handles handle err redirect.
|
|
1828
|
+
*
|
|
1829
|
+
* @param e - Change event received from the active Docs input.
|
|
1830
|
+
*/
|
|
1831
|
+
const handleErrRedirect = (e) => {
|
|
1832
|
+
const type = (e.detail || {}).type; // '403' or '404'
|
|
1833
|
+
console.warn(`[Scaffold] Caught docs:error:redirect event (${type}). Performing soft redirect...`);
|
|
1834
|
+
toast.error(type === '403' ? 'Permission denied.' : 'Document not found.');
|
|
1835
|
+
if (type === '403') {
|
|
1836
|
+
const url = new URL(window.location.href);
|
|
1837
|
+
url.searchParams.delete('403');
|
|
1838
|
+
url.searchParams.delete('404');
|
|
1839
|
+
window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
// find first leaf to fall back to securely
|
|
1843
|
+
/**
|
|
1844
|
+
* Handles get first leaf.
|
|
1845
|
+
*
|
|
1846
|
+
* @param nodes - Docs tree nodes being rendered in the navigation.
|
|
1847
|
+
*/
|
|
1848
|
+
const cFallback = ((function firstLeaf(nodes) {
|
|
1849
|
+
for (const n of nodes) {
|
|
1850
|
+
if (n.type === 'leaf')
|
|
1851
|
+
return n.url || null;
|
|
1852
|
+
if (n.type === 'branch' && n.children) {
|
|
1853
|
+
const found = firstLeaf(n.children);
|
|
1854
|
+
if (found)
|
|
1855
|
+
return found;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return null;
|
|
1859
|
+
})(tree) || docsRootPath()).split('?')[0];
|
|
1860
|
+
const url = new URL(window.location.href);
|
|
1861
|
+
url.pathname = cFallback;
|
|
1862
|
+
url.searchParams.delete('403');
|
|
1863
|
+
url.searchParams.delete('404');
|
|
1864
|
+
window.history.replaceState(null, '', url.toString());
|
|
1865
|
+
document.dispatchEvent(new CustomEvent('docs:navigate', { detail: { url: cFallback, pathname: cFallback } }));
|
|
1866
|
+
};
|
|
1867
|
+
document.addEventListener('docs:error:redirect', handleErrRedirect);
|
|
1868
|
+
return () => {
|
|
1869
|
+
document.removeEventListener('docs:navigate', handleNav);
|
|
1870
|
+
window.removeEventListener('docs:navigate', handleNav);
|
|
1871
|
+
document.removeEventListener('docs:error:redirect', handleErrRedirect);
|
|
1872
|
+
};
|
|
1873
|
+
}, [tree]);
|
|
1874
|
+
// keep the selected mode and URL query aligned so reloads preserve the active surface
|
|
1875
|
+
useEffect(() => {
|
|
1876
|
+
if (modeSynced.current)
|
|
1877
|
+
return;
|
|
1878
|
+
modeSynced.current = true;
|
|
1879
|
+
if (typeof window === 'undefined')
|
|
1880
|
+
return;
|
|
1881
|
+
const cur = new URL(window.location.href).searchParams.get('mode');
|
|
1882
|
+
if (!writable && stored === 'edit')
|
|
1883
|
+
$mode.set('view');
|
|
1884
|
+
if (cur && ['view', 'edit', 'attachments'].includes(cur) && (writable || cur !== 'edit')) {
|
|
1885
|
+
pendingMode.current = cur;
|
|
1886
|
+
if (cur !== stored)
|
|
1887
|
+
$mode.set(cur);
|
|
1888
|
+
}
|
|
1889
|
+
else if (!cur && stored !== 'view')
|
|
1890
|
+
$mode.set('view');
|
|
1891
|
+
}, [stored, writable]);
|
|
1892
|
+
useEffect(() => {
|
|
1893
|
+
if (pendingMode.current && stored !== pendingMode.current)
|
|
1894
|
+
return;
|
|
1895
|
+
pendingMode.current = null;
|
|
1896
|
+
const url = new URL(window.location.href);
|
|
1897
|
+
const cur = url.searchParams.get('mode');
|
|
1898
|
+
if (!writable) {
|
|
1899
|
+
if (cur === 'edit') {
|
|
1900
|
+
url.searchParams.delete('mode');
|
|
1901
|
+
window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
|
|
1902
|
+
}
|
|
1903
|
+
if (stored === 'edit')
|
|
1904
|
+
$mode.set('view');
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
if (cur === stored)
|
|
1908
|
+
return;
|
|
1909
|
+
if (stored === 'view')
|
|
1910
|
+
url.searchParams.delete('mode');
|
|
1911
|
+
else
|
|
1912
|
+
url.searchParams.set('mode', stored);
|
|
1913
|
+
window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
|
|
1914
|
+
}, [stored, writable]);
|
|
1915
|
+
// Ensure URL matches document viewed (without appending duplicate query strings)
|
|
1916
|
+
useEffect(() => {
|
|
1917
|
+
const url = new URL(window.location.href);
|
|
1918
|
+
const cCurrPath = currDoc.pathname?.split('?')[0];
|
|
1919
|
+
const nPath = cCurrPath ? keepDeepPath(url.pathname, cCurrPath) : '';
|
|
1920
|
+
if (nPath && url.pathname !== nPath) {
|
|
1921
|
+
console.info('[Docs:url-sync]', { current: url.pathname, next: cCurrPath, path: currDoc.path, resolved: nPath });
|
|
1922
|
+
url.pathname = nPath;
|
|
1923
|
+
window.history.replaceState(null, '', url.toString());
|
|
1924
|
+
}
|
|
1925
|
+
}, [currDoc.pathname]);
|
|
1926
|
+
// PROCESS
|
|
1927
|
+
const a = !hydrated ? (initialMode ?? 'view') : !writable && stored === 'edit' ? 'view' : stored;
|
|
1928
|
+
// unify all loading scenarios into a single boolean
|
|
1929
|
+
const loadingState = { isSaving: toolbar?.busy === 'saving' && !toolbar?.optimistic, transition: useTransitionState() };
|
|
1930
|
+
const isLoading = !hydrated || loadingState.isSaving || loadingState.transition !== 'idle';
|
|
1931
|
+
useEffect(() => {
|
|
1932
|
+
setHydrated(true);
|
|
1933
|
+
if (!writable && stored === 'edit')
|
|
1934
|
+
$mode.set('view');
|
|
1935
|
+
}, [currDoc.pathname, writable, stored]);
|
|
1936
|
+
useEffect(() => {
|
|
1937
|
+
const url = new URL(window.location.href);
|
|
1938
|
+
if (!(url.searchParams.get('403') === 'true'))
|
|
1939
|
+
return;
|
|
1940
|
+
console.warn('[Docs:403]', { pathname: url.pathname, search: url.search });
|
|
1941
|
+
toast.error('Permission denied.');
|
|
1942
|
+
url.searchParams.delete('403');
|
|
1943
|
+
url.searchParams.delete('404');
|
|
1944
|
+
window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
|
|
1945
|
+
}, []);
|
|
1946
|
+
// RENDER
|
|
1947
|
+
if (standalone) {
|
|
1948
|
+
return (_jsx(TransitionShell, { className: 'size-full min-w-0 max-w-none overflow-visible text-inherit print:m-0 print:h-auto print:min-h-0 print:w-full print:overflow-visible print:p-0', mode: 'view', children: _jsx("div", { "data-docs-shell": true, "data-docs-mode": 'view', className: 'w-full', children: _jsx("div", { className: cn('mx-auto w-full px-6 md:px-10', full ? 'max-w-none' : 'max-w-5xl'), children: _jsx("div", { "data-docs-body": true, className: 'relative h-full min-h-[50vh] text-primary-12', children: _jsx("div", { className: 'prose max-w-none pb-40 pt-8 transition-opacity duration-200', "data-docs-body-content": true, "data-docs-view-root": 'true', children: _jsx(ErrorBoundary, { name: 'StandaloneBody', fallback: () => _jsx(AlertError, { title: 'Standalone Surface Crashed', description: 'The standalone documentation surface crashed during rendering.' }), children: _jsx(View, { body: isDocBody(currDoc.content) ? currDoc.content : [], resetKey: currDoc.path ?? currDoc.pathname }) }) }) }) }) }) }));
|
|
1949
|
+
}
|
|
1950
|
+
return (_jsxs(SidebarProvider, { className: 'terra-docs-workspace font-sans', open: hydrated ? navigationState.open : true, onOpenChange: n => {
|
|
1951
|
+
$sidebar.set(n);
|
|
1952
|
+
}, children: [_jsx(Toaster, { richColors: true }), _jsx(Nav, { docs: tree, pathname: currDoc.pathname, permission: permission, userData: userData ?? null, siteTitle: getDocsHostAdapter()?.siteTitle ?? 'Terra Symposium', siteDescription: getDocsHostAdapter()?.siteDescription, homePath: getDocsHostAdapter()?.homePath ?? docsRootPath() }), _jsxs(SidebarInset, { className: 'relative flex min-h-svh min-w-0 flex-col print:min-h-0', children: [_jsx(ErrorBoundary, { name: 'NavHeader', children: _jsxs("header", { className: 'flex h-14 shrink-0 items-center border-b border-primary-6 bg-primary-2 px-5 md:hidden print:hidden', children: [_jsx(SidebarTrigger, {}), _jsx(Separator, { orientation: 'vertical', className: 'mx-2 h-4' }), _jsx("span", { className: 'font-serif text-lg', children: getDocsHostAdapter()?.siteTitle ?? 'Terra Symposium' })] }) }), _jsxs("div", { className: cn('relative flex min-h-0 flex-1 flex-col', isLoading && 'overflow-hidden'), children: [hydrated && !loadingState.isSaving && (_jsx(TransitionShell, { className: cn('size-full min-w-0 max-w-none overflow-visible text-inherit print:m-0 print:h-auto print:min-h-0 print:w-full print:overflow-visible print:p-0', loadingState.isSaving ? 'hidden' : isLoading ? 'pointer-events-none absolute inset-0 overflow-hidden opacity-0' : 'block'), mode: a, path: currDoc.path, children: _jsx(EditProvider, { permission: permission, ...currDoc, children: _jsxs("div", { className: 'w-full', children: [_jsx(EditorSurfaces.Head, { compact: a === 'edit', editable: writable && a !== 'view', title: currDoc.title, full: full, ...(currDoc.description !== undefined && { description: currDoc.description }), contributors: currDoc.contributors, updatedAt: currDoc.updatedAt }), _jsx("div", { className: 'w-full', children: _jsx("div", { className: cn('mx-auto w-full px-6 md:px-10', full ? 'max-w-none' : 'max-w-5xl'), children: _jsxs("div", { "data-docs-body": true, className: 'relative h-full min-h-[50vh] text-primary-12', children: [_jsx(Toolbar, { toolbar: toolbar, activeMode: a, editable: writable }), _jsx("div", { className: cn('prose max-w-none pb-40 transition-opacity duration-200', a === 'edit' ? 'pt-2' : 'pt-8'), "data-docs-body-content": true, "data-docs-view-root": a === 'view' ? 'true' : undefined, children: _jsx(ErrorBoundary, { name: 'EditorBody', resetKey: `${a}:${currDoc.path ?? ''}`, fallback: () => (_jsx("div", { className: 'p-8', children: _jsx(AlertError, { title: 'Editor Surface Crashed', description: 'The workspace editor canvas encountered a rendering crash. You can safely switch to attachments view to recover.' }) })), children: hydrated &&
|
|
1953
|
+
(a === 'view' ? (_jsx(View, { body: isDocBody(currDoc.content) ? currDoc.content : [], resetKey: currDoc.path ?? currDoc.pathname })) : a === 'edit' ? (_jsx(EditorSurfaces.Edit, {})) : a === 'attachments' ? (_jsx(Attachments, { dPath: currDoc.path ?? '', tree: tree })) : (null)) }) })] }) }) })] }) }, currDoc.pathname) })), isLoading && _jsx(Loader, { className: 'absolute inset-0 z-10' })] }), _jsx(Dialog, { open: dest.open, onOpenChange: o => {
|
|
1954
|
+
if (!o)
|
|
1955
|
+
destRes.current?.(null);
|
|
1956
|
+
setDest(p => ({ ...p, open: o }));
|
|
1957
|
+
}, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Save document" }), _jsx(DialogDescription, { children: "Choose where to save this document." })] }), _jsxs("div", { className: 'space-y-4', children: [_jsx(Label, { children: "Page title" }), _jsx(Input, { value: dest.title, onChange: e => { setDest(p => ({ ...p, title: e.currentTarget.value })); } }), _jsx(Label, { children: "Section" }), _jsxs(Select, { value: dest.section, onValueChange: v => { setDest(p => ({ ...p, section: v, group: sects.find(s => s.origName === v)?.children?.find(c => c.type === 'branch')?.origName || '' })); }, children: [_jsx(SelectTrigger, { "aria-label": 'Select section', children: _jsx(SelectValue, { placeholder: 'Select section' }) }), _jsx(SelectContent, { children: sects.map(s => (_jsx(SelectItem, { value: s.origName, children: s.title || s.origName }, s.origName))) })] }), _jsx(Label, { children: "Group" }), _jsxs(Select, { value: dest.group, onValueChange: g => { setDest(p => ({ ...p, group: g })); }, children: [_jsx(SelectTrigger, { "aria-label": 'Select group', children: _jsx(SelectValue, { placeholder: 'Select group' }) }), _jsx(SelectContent, { children: destActions.groups.map(s => (_jsx(SelectItem, { value: s.origName, children: s.title || s.origName }, s.origName))) })] }), _jsx(Button, { variant: 'outline', onClick: () => { setDest(p => ({ ...p, newSection: !p.newSection, section: '', group: '' })); }, children: dest.newSection ? 'Use existing section' : 'Create new section' }), dest.newSection && (_jsxs(_Fragment, { children: [_jsx(Label, { children: "New section name" }), _jsx(Input, { value: dest.section, onChange: e => { setDest(p => ({ ...p, section: e.currentTarget.value })); } }), _jsx(Label, { children: "New group name" }), _jsx(Input, { value: dest.group, onChange: e => { setDest(p => ({ ...p, group: e.currentTarget.value })); } })] }))] }), _jsx(DialogFooter, { children: _jsx(Button, { onClick: destActions.finish, disabled: !dest.title.trim() || !dest.section || !dest.group, children: "Save" }) })] }) })] })] }));
|
|
1958
|
+
}
|
|
1959
|
+
//# sourceMappingURL=_main.js.map
|