create-starlight-cms 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -0
- package/THIRD_PARTY_NOTICES.md +4 -0
- package/bin/index.js +147 -0
- package/package.json +46 -0
- package/template/.nvmrc +1 -0
- package/template/LICENSE +21 -0
- package/template/README.md +68 -0
- package/template/THIRD_PARTY_NOTICES.md +12 -0
- package/template/_gitignore +14 -0
- package/template/astro.config.mjs +26 -0
- package/template/migrations/0001_schema.sql +73 -0
- package/template/migrations/0002_publish_delivery.sql +16 -0
- package/template/package-lock.json.template +9123 -0
- package/template/package.json +89 -0
- package/template/scripts/build-admin.mjs +38 -0
- package/template/scripts/check-linux-bindings.mjs +18 -0
- package/template/scripts/dev.mjs +94 -0
- package/template/src/admin/admin.scss +182 -0
- package/template/src/admin/app.ts +130 -0
- package/template/src/admin/client.tsx +684 -0
- package/template/src/admin/components/tiptap-extension/node-background-extension.ts +150 -0
- package/template/src/admin/components/tiptap-icons/align-center-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/align-justify-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/align-left-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/align-right-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/arrow-left-icon.tsx +24 -0
- package/template/src/admin/components/tiptap-icons/arrow-right-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/ban-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/blockquote-icon.tsx +44 -0
- package/template/src/admin/components/tiptap-icons/bold-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/case-sensitive-icon.tsx +32 -0
- package/template/src/admin/components/tiptap-icons/check-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/chevron-down-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/chevron-up-icon.tsx +22 -0
- package/template/src/admin/components/tiptap-icons/close-icon.tsx +24 -0
- package/template/src/admin/components/tiptap-icons/code-block-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/code2-icon.tsx +32 -0
- package/template/src/admin/components/tiptap-icons/corner-down-left-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/external-link-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/heading-five-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/heading-four-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/heading-icon.tsx +24 -0
- package/template/src/admin/components/tiptap-icons/heading-one-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/heading-six-icon.tsx +30 -0
- package/template/src/admin/components/tiptap-icons/heading-three-icon.tsx +36 -0
- package/template/src/admin/components/tiptap-icons/heading-two-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/highlighter-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/image-plus-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/italic-icon.tsx +24 -0
- package/template/src/admin/components/tiptap-icons/link-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/list-icon.tsx +56 -0
- package/template/src/admin/components/tiptap-icons/list-ordered-icon.tsx +56 -0
- package/template/src/admin/components/tiptap-icons/list-todo-icon.tsx +50 -0
- package/template/src/admin/components/tiptap-icons/moon-star-icon.tsx +30 -0
- package/template/src/admin/components/tiptap-icons/redo2-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/search-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/strike-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/subscript-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/sun-icon.tsx +58 -0
- package/template/src/admin/components/tiptap-icons/superscript-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/trash-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/underline-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/undo2-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/whole-word-icon.tsx +36 -0
- package/template/src/admin/components/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
- package/template/src/admin/components/tiptap-node/code-block-node/code-block-node.scss +54 -0
- package/template/src/admin/components/tiptap-node/heading-node/heading-node.scss +45 -0
- package/template/src/admin/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +14 -0
- package/template/src/admin/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
- package/template/src/admin/components/tiptap-node/image-node/image-node.scss +35 -0
- package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node-extension.ts +161 -0
- package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
- package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node.tsx +554 -0
- package/template/src/admin/components/tiptap-node/image-upload-node/index.tsx +1 -0
- package/template/src/admin/components/tiptap-node/list-node/list-node.scss +208 -0
- package/template/src/admin/components/tiptap-node/paragraph-node/paragraph-node.scss +217 -0
- package/template/src/admin/components/tiptap-templates/simple/NOTICE.md +14 -0
- package/template/src/admin/components/tiptap-templates/simple/data/content.json +477 -0
- package/template/src/admin/components/tiptap-templates/simple/simple-editor.scss +97 -0
- package/template/src/admin/components/tiptap-templates/simple/simple-editor.tsx +348 -0
- package/template/src/admin/components/tiptap-templates/simple/theme-toggle.tsx +44 -0
- package/template/src/admin/components/tiptap-ui/blockquote-button/blockquote-button.tsx +122 -0
- package/template/src/admin/components/tiptap-ui/blockquote-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/blockquote-button/use-blockquote.ts +273 -0
- package/template/src/admin/components/tiptap-ui/code-block-button/code-block-button.tsx +122 -0
- package/template/src/admin/components/tiptap-ui/code-block-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/code-block-button/use-code-block.ts +283 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx +170 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-button/use-color-highlight.ts +380 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +231 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-popover/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui/heading-button/heading-button.tsx +124 -0
- package/template/src/admin/components/tiptap-ui/heading-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/heading-button/use-heading.ts +348 -0
- package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +128 -0
- package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +132 -0
- package/template/src/admin/components/tiptap-ui/image-upload-button/image-upload-button.tsx +130 -0
- package/template/src/admin/components/tiptap-ui/image-upload-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/image-upload-button/use-image-upload.ts +197 -0
- package/template/src/admin/components/tiptap-ui/link-popover/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/link-popover/link-popover.scss +16 -0
- package/template/src/admin/components/tiptap-ui/link-popover/link-popover.tsx +315 -0
- package/template/src/admin/components/tiptap-ui/link-popover/use-link-popover.ts +297 -0
- package/template/src/admin/components/tiptap-ui/list-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/list-button/list-button.tsx +120 -0
- package/template/src/admin/components/tiptap-ui/list-button/use-list.ts +353 -0
- package/template/src/admin/components/tiptap-ui/list-dropdown-menu/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +126 -0
- package/template/src/admin/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +220 -0
- package/template/src/admin/components/tiptap-ui/mark-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/mark-button/mark-button.tsx +122 -0
- package/template/src/admin/components/tiptap-ui/mark-button/use-mark.ts +219 -0
- package/template/src/admin/components/tiptap-ui/search-and-replace/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/search-and-replace/search-and-replace.scss +300 -0
- package/template/src/admin/components/tiptap-ui/search-and-replace/search-and-replace.tsx +718 -0
- package/template/src/admin/components/tiptap-ui/search-and-replace/use-search-and-replace.ts +528 -0
- package/template/src/admin/components/tiptap-ui/text-align-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/text-align-button/text-align-button.tsx +144 -0
- package/template/src/admin/components/tiptap-ui/text-align-button/use-text-align.ts +237 -0
- package/template/src/admin/components/tiptap-ui/undo-redo-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx +125 -0
- package/template/src/admin/components/tiptap-ui/undo-redo-button/use-undo-redo.ts +200 -0
- package/template/src/admin/components/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
- package/template/src/admin/components/tiptap-ui-primitive/badge/badge-group.scss +16 -0
- package/template/src/admin/components/tiptap-ui-primitive/badge/badge.scss +99 -0
- package/template/src/admin/components/tiptap-ui-primitive/badge/badge.tsx +44 -0
- package/template/src/admin/components/tiptap-ui-primitive/badge/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/button/button-colors.scss +613 -0
- package/template/src/admin/components/tiptap-ui-primitive/button/button.scss +390 -0
- package/template/src/admin/components/tiptap-ui-primitive/button/button.tsx +158 -0
- package/template/src/admin/components/tiptap-ui-primitive/button/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/button-group/button-group.scss +89 -0
- package/template/src/admin/components/tiptap-ui-primitive/button-group/button-group.tsx +72 -0
- package/template/src/admin/components/tiptap-ui-primitive/button-group/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/card/card.scss +74 -0
- package/template/src/admin/components/tiptap-ui-primitive/card/card.tsx +79 -0
- package/template/src/admin/components/tiptap-ui-primitive/card/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +152 -0
- package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +262 -0
- package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/input/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/input/input.scss +41 -0
- package/template/src/admin/components/tiptap-ui-primitive/input/input.tsx +22 -0
- package/template/src/admin/components/tiptap-ui-primitive/input-group/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/input-group/input-group.scss +225 -0
- package/template/src/admin/components/tiptap-ui-primitive/input-group/input-group.tsx +135 -0
- package/template/src/admin/components/tiptap-ui-primitive/popover/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/popover/popover.scss +64 -0
- package/template/src/admin/components/tiptap-ui-primitive/popover/popover.tsx +35 -0
- package/template/src/admin/components/tiptap-ui-primitive/separator/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/separator/separator.scss +23 -0
- package/template/src/admin/components/tiptap-ui-primitive/separator/separator.tsx +28 -0
- package/template/src/admin/components/tiptap-ui-primitive/spacer/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/spacer/spacer.tsx +24 -0
- package/template/src/admin/components/tiptap-ui-primitive/switch/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/switch/switch.scss +86 -0
- package/template/src/admin/components/tiptap-ui-primitive/switch/switch.tsx +29 -0
- package/template/src/admin/components/tiptap-ui-primitive/textarea/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/textarea/textarea.scss +42 -0
- package/template/src/admin/components/tiptap-ui-primitive/textarea/textarea.tsx +16 -0
- package/template/src/admin/components/tiptap-ui-primitive/toolbar/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
- package/template/src/admin/components/tiptap-ui-primitive/toolbar/toolbar.tsx +203 -0
- package/template/src/admin/components/tiptap-ui-primitive/tooltip/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
- package/template/src/admin/components/tiptap-ui-primitive/tooltip/tooltip.tsx +241 -0
- package/template/src/admin/hooks/use-composed-ref.ts +46 -0
- package/template/src/admin/hooks/use-cursor-visibility.ts +69 -0
- package/template/src/admin/hooks/use-element-rect.ts +166 -0
- package/template/src/admin/hooks/use-is-breakpoint.ts +35 -0
- package/template/src/admin/hooks/use-menu-navigation.ts +204 -0
- package/template/src/admin/hooks/use-scrolling.ts +75 -0
- package/template/src/admin/hooks/use-throttled-callback.ts +48 -0
- package/template/src/admin/hooks/use-tiptap-editor.ts +69 -0
- package/template/src/admin/hooks/use-unmount.ts +21 -0
- package/template/src/admin/hooks/use-window-size.ts +93 -0
- package/template/src/admin/html.ts +17 -0
- package/template/src/admin/lib/tiptap-utils.ts +652 -0
- package/template/src/admin/navigation-tree.tsx +82 -0
- package/template/src/admin/preview.ts +73 -0
- package/template/src/admin/styles/_cms-theme.scss +58 -0
- package/template/src/admin/styles/_keyframe-animations.scss +91 -0
- package/template/src/admin/styles/_variables.scss +298 -0
- package/template/src/assets/favicon.svg +1 -0
- package/template/src/assets/logo.svg +1 -0
- package/template/src/astro-env.d.ts +1 -0
- package/template/src/content.config.ts +12 -0
- package/template/src/db/bootstrap.ts +36 -0
- package/template/src/db/client.ts +7 -0
- package/template/src/db/schema.ts +45 -0
- package/template/src/documents/service.ts +178 -0
- package/template/src/documents/validation.ts +9 -0
- package/template/src/env.ts +9 -0
- package/template/src/index.ts +10 -0
- package/template/src/locales.ts +10 -0
- package/template/src/media/service.ts +120 -0
- package/template/src/navigation/service.ts +171 -0
- package/template/src/pages/[locale]/cms-preview-shell.astro +12 -0
- package/template/src/pages/cms-preview-shell.astro +5 -0
- package/template/src/publish/deploy-hook.ts +42 -0
- package/template/src/publish/record.ts +20 -0
- package/template/src/publish/service.ts +135 -0
- package/template/src/scss.d.ts +5 -0
- package/template/src/site.config.ts +7 -0
- package/template/src/sql.d.ts +4 -0
- package/template/src/starlight/cms-origin.ts +4 -0
- package/template/src/starlight/loader.ts +21 -0
- package/template/src/starlight/preview-render.ts +162 -0
- package/template/src/starlight/preview-shell.astro +14 -0
- package/template/src/starlight/render.ts +59 -0
- package/template/src/starlight/schema.ts +34 -0
- package/template/src/starlight/sidebar.ts +35 -0
- package/template/src/starlight/snapshot.ts +47 -0
- package/template/src/starlight/source.ts +26 -0
- package/template/src/starlight/tiptap.ts +29 -0
- package/template/src/styles/starlight.css +12 -0
- package/template/test-files/content.test.template.ts +82 -0
- package/template/test-files/deploy-hook.test.template.ts +32 -0
- package/template/test-files/media.test.template.ts +39 -0
- package/template/test-files/publishing.integration.test.template.ts +156 -0
- package/template/tsconfig.admin.json +18 -0
- package/template/tsconfig.json +23 -0
- package/template/worker-configuration.d.ts +15389 -0
- package/template/wrangler.jsonc +26 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useRef } from "react"
|
|
4
|
+
import { useHotkeys } from "react-hotkeys-hook"
|
|
5
|
+
|
|
6
|
+
// --- Hooks ---
|
|
7
|
+
import { useComposedRef } from "@/hooks/use-composed-ref"
|
|
8
|
+
|
|
9
|
+
// --- Icons ---
|
|
10
|
+
import { ArrowRightIcon } from "@/components/tiptap-icons/arrow-right-icon"
|
|
11
|
+
import { CaseSensitiveIcon } from "@/components/tiptap-icons/case-sensitive-icon"
|
|
12
|
+
import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon"
|
|
13
|
+
import { ChevronUpIcon } from "@/components/tiptap-icons/chevron-up-icon"
|
|
14
|
+
import { CloseIcon } from "@/components/tiptap-icons/close-icon"
|
|
15
|
+
import { ExternalLinkIcon } from "@/components/tiptap-icons/external-link-icon"
|
|
16
|
+
import { SearchIcon } from "@/components/tiptap-icons/search-icon"
|
|
17
|
+
import { WholeWordIcon } from "@/components/tiptap-icons/whole-word-icon"
|
|
18
|
+
|
|
19
|
+
// --- Lib ---
|
|
20
|
+
import { cn } from "@/lib/tiptap-utils"
|
|
21
|
+
|
|
22
|
+
// --- Tiptap UI ---
|
|
23
|
+
import type { UseSearchAndReplaceConfig } from "@/components/tiptap-ui/search-and-replace"
|
|
24
|
+
import {
|
|
25
|
+
NEXT_RESULT_SHORTCUT_KEY,
|
|
26
|
+
PREVIOUS_RESULT_SHORTCUT_KEY,
|
|
27
|
+
SEARCH_AND_REPLACE_SHORTCUT_KEY,
|
|
28
|
+
useSearchAndReplace,
|
|
29
|
+
} from "@/components/tiptap-ui/search-and-replace"
|
|
30
|
+
|
|
31
|
+
// --- UI Primitives ---
|
|
32
|
+
import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
|
|
33
|
+
import { Button } from "@/components/tiptap-ui-primitive/button"
|
|
34
|
+
import { ButtonGroup } from "@/components/tiptap-ui-primitive/button-group"
|
|
35
|
+
import {
|
|
36
|
+
Card,
|
|
37
|
+
CardBody,
|
|
38
|
+
CardFooter,
|
|
39
|
+
CardHeader,
|
|
40
|
+
} from "@/components/tiptap-ui-primitive/card"
|
|
41
|
+
import {
|
|
42
|
+
InputGroup,
|
|
43
|
+
InputGroupAddon,
|
|
44
|
+
InputGroupInput,
|
|
45
|
+
} from "@/components/tiptap-ui-primitive/input-group"
|
|
46
|
+
import { Separator } from "@/components/tiptap-ui-primitive/separator"
|
|
47
|
+
import { Switch } from "@/components/tiptap-ui-primitive/switch"
|
|
48
|
+
|
|
49
|
+
// --- Styles ---
|
|
50
|
+
import "@/components/tiptap-ui/search-and-replace/search-and-replace.scss"
|
|
51
|
+
|
|
52
|
+
export interface SearchAndReplaceProps
|
|
53
|
+
extends
|
|
54
|
+
Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
|
|
55
|
+
UseSearchAndReplaceConfig {
|
|
56
|
+
/**
|
|
57
|
+
* Whether the panel is active. While `false` the panel stays mounted but
|
|
58
|
+
* hidden: highlights are cleared and the entered terms are kept, so
|
|
59
|
+
* toggling back on restores the previous search. Unmounting the panel
|
|
60
|
+
* instead clears the search entirely.
|
|
61
|
+
* @default true
|
|
62
|
+
*/
|
|
63
|
+
open?: boolean
|
|
64
|
+
/**
|
|
65
|
+
* Called when the panel requests to close (close button or Escape). The
|
|
66
|
+
* panel does not position or dismiss itself — rendering, placement and
|
|
67
|
+
* open state are owned by the consumer.
|
|
68
|
+
*/
|
|
69
|
+
onClose?: () => void
|
|
70
|
+
/**
|
|
71
|
+
* Called when the built-in Mod+F shortcut fires while the panel is hidden.
|
|
72
|
+
* Wire it to your open state and the shortcut works out of the box;
|
|
73
|
+
* without it the shortcut only refocuses the search input while the panel
|
|
74
|
+
* is already open.
|
|
75
|
+
*/
|
|
76
|
+
onOpen?: () => void
|
|
77
|
+
/**
|
|
78
|
+
* Whether the built-in Mod+F shortcut is bound. While bound (and `onOpen`
|
|
79
|
+
* is wired), it replaces the browser's find-in-page everywhere on the
|
|
80
|
+
* page — an app with its own search should never surface the native one.
|
|
81
|
+
* @default true
|
|
82
|
+
*/
|
|
83
|
+
enableShortcut?: boolean
|
|
84
|
+
/**
|
|
85
|
+
* Whether the search input focuses itself when the panel becomes open.
|
|
86
|
+
* @default true
|
|
87
|
+
*/
|
|
88
|
+
autoFocusSearch?: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type SearchAndReplaceContentProps = SearchAndReplaceProps
|
|
92
|
+
|
|
93
|
+
function isModKey(event: React.KeyboardEvent): boolean {
|
|
94
|
+
return event.metaKey || event.ctrlKey
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const REGEX_DOCS_URL =
|
|
98
|
+
"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions"
|
|
99
|
+
|
|
100
|
+
const REGEX_SEARCH_EXAMPLES = [
|
|
101
|
+
{ label: "Any middle letter:", pattern: "c.t" },
|
|
102
|
+
{ label: "Either term:", pattern: "cat|tiptap" },
|
|
103
|
+
{ label: "Uppercase or lowercase C:", pattern: "[Cc]at" },
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
const REGEX_REPLACE_EXAMPLES = [
|
|
107
|
+
{ pattern: "TipTap|tiptap|TIPTAP", replacement: "Tiptap" },
|
|
108
|
+
{ pattern: "(cat|tiptap)", replacement: '"$1"' },
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Example row for the regex explanation box. Deliberately local to this
|
|
113
|
+
* box — a small ghost-style button whose label mixes plain text with code
|
|
114
|
+
* chips is too specific to become a shared primitive.
|
|
115
|
+
*/
|
|
116
|
+
function RegexExampleButton({
|
|
117
|
+
pattern,
|
|
118
|
+
onApply,
|
|
119
|
+
children,
|
|
120
|
+
}: {
|
|
121
|
+
pattern: string
|
|
122
|
+
onApply: () => void
|
|
123
|
+
children: React.ReactNode
|
|
124
|
+
}) {
|
|
125
|
+
return (
|
|
126
|
+
<ButtonGroup
|
|
127
|
+
className="tiptap-search-replace-regex-example-row"
|
|
128
|
+
orientation="horizontal"
|
|
129
|
+
>
|
|
130
|
+
<Button
|
|
131
|
+
type="button"
|
|
132
|
+
className="tiptap-search-replace-regex-example-button"
|
|
133
|
+
data-regex-example={pattern}
|
|
134
|
+
onClick={onApply}
|
|
135
|
+
variant="ghost"
|
|
136
|
+
size="small"
|
|
137
|
+
>
|
|
138
|
+
<span className="tiptap-button-text">{children}</span>
|
|
139
|
+
<ArrowRightIcon className="tiptap-button-icon" aria-hidden="true" />
|
|
140
|
+
</Button>
|
|
141
|
+
</ButtonGroup>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Search and replace trigger button for toolbars. Purely presentational —
|
|
147
|
+
* wire `onClick` to show the `SearchAndReplace` panel wherever your layout
|
|
148
|
+
* places it (the panel itself binds Mod+F via its `onOpen` prop).
|
|
149
|
+
*/
|
|
150
|
+
export const SearchAndReplaceButton = forwardRef<
|
|
151
|
+
HTMLButtonElement,
|
|
152
|
+
ButtonProps
|
|
153
|
+
>(({ className, children, ...props }, ref) => {
|
|
154
|
+
return (
|
|
155
|
+
<Button
|
|
156
|
+
type="button"
|
|
157
|
+
className={className}
|
|
158
|
+
variant="ghost"
|
|
159
|
+
role="button"
|
|
160
|
+
tabIndex={-1}
|
|
161
|
+
aria-label="Search and replace"
|
|
162
|
+
tooltip="Search and replace"
|
|
163
|
+
shortcutKeys={SEARCH_AND_REPLACE_SHORTCUT_KEY}
|
|
164
|
+
ref={ref}
|
|
165
|
+
{...props}
|
|
166
|
+
>
|
|
167
|
+
{children || <SearchIcon className="tiptap-button-icon" />}
|
|
168
|
+
</Button>
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
SearchAndReplaceButton.displayName = "SearchAndReplaceButton"
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Search and replace panel for Tiptap editors.
|
|
176
|
+
*
|
|
177
|
+
* Deliberately unpositioned: render it inside a popover, docked in a corner
|
|
178
|
+
* (Notion-style), inline in a collapsed mobile toolbar — the wrapper is up to
|
|
179
|
+
* you. The panel only manages the search itself.
|
|
180
|
+
*
|
|
181
|
+
* For fully custom UIs, use the `useSearchAndReplace` hook instead.
|
|
182
|
+
*/
|
|
183
|
+
export const SearchAndReplace = forwardRef<
|
|
184
|
+
HTMLDivElement,
|
|
185
|
+
SearchAndReplaceProps
|
|
186
|
+
>(
|
|
187
|
+
(
|
|
188
|
+
{
|
|
189
|
+
editor: providedEditor,
|
|
190
|
+
hideWhenUnavailable = false,
|
|
191
|
+
scrollIntoViewOptions,
|
|
192
|
+
open = true,
|
|
193
|
+
onClose,
|
|
194
|
+
onOpen,
|
|
195
|
+
enableShortcut = true,
|
|
196
|
+
autoFocusSearch = true,
|
|
197
|
+
className,
|
|
198
|
+
...divProps
|
|
199
|
+
},
|
|
200
|
+
ref
|
|
201
|
+
) => {
|
|
202
|
+
const searchAndReplace = useSearchAndReplace({
|
|
203
|
+
editor: providedEditor,
|
|
204
|
+
hideWhenUnavailable,
|
|
205
|
+
scrollIntoViewOptions,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const {
|
|
209
|
+
isVisible,
|
|
210
|
+
resultCountLabel,
|
|
211
|
+
searchTerm,
|
|
212
|
+
replaceTerm,
|
|
213
|
+
caseSensitive,
|
|
214
|
+
wholeWord,
|
|
215
|
+
useRegex,
|
|
216
|
+
canSearch,
|
|
217
|
+
canNavigate,
|
|
218
|
+
canReplace,
|
|
219
|
+
canReplaceAll,
|
|
220
|
+
setSearchTerm,
|
|
221
|
+
setReplaceTerm,
|
|
222
|
+
toggleCaseSensitive,
|
|
223
|
+
toggleWholeWord,
|
|
224
|
+
toggleUseRegex,
|
|
225
|
+
goToNext,
|
|
226
|
+
goToPrevious,
|
|
227
|
+
replaceCurrent,
|
|
228
|
+
replaceAll,
|
|
229
|
+
applySearch,
|
|
230
|
+
suspendSearch,
|
|
231
|
+
} = searchAndReplace
|
|
232
|
+
|
|
233
|
+
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
234
|
+
const panelRef = useRef<HTMLDivElement>(null)
|
|
235
|
+
const composedPanelRef = useComposedRef(panelRef, ref)
|
|
236
|
+
|
|
237
|
+
const focusSearchInput = useCallback(() => {
|
|
238
|
+
const input = searchInputRef.current
|
|
239
|
+
if (!input) return
|
|
240
|
+
input.focus()
|
|
241
|
+
input.select()
|
|
242
|
+
}, [])
|
|
243
|
+
|
|
244
|
+
// Example buttons fill the inputs so the pattern can be tested right
|
|
245
|
+
// away; the extension debounce then applies the search.
|
|
246
|
+
const applyRegexExample = useCallback(
|
|
247
|
+
(pattern: string, replacement?: string) => {
|
|
248
|
+
setSearchTerm(pattern)
|
|
249
|
+
if (replacement !== undefined) {
|
|
250
|
+
setReplaceTerm(replacement)
|
|
251
|
+
}
|
|
252
|
+
focusSearchInput()
|
|
253
|
+
},
|
|
254
|
+
[setSearchTerm, setReplaceTerm, focusSearchInput]
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
// Built-in Mod+F: with a custom search present, the browser's
|
|
258
|
+
// find-in-page is replaced everywhere on the page, no matter where
|
|
259
|
+
// focus sits. Opens the panel via `onOpen` when hidden, refocuses the
|
|
260
|
+
// search input while already open. Only an unwired `onOpen` falls back
|
|
261
|
+
// to the native find.
|
|
262
|
+
useHotkeys(
|
|
263
|
+
SEARCH_AND_REPLACE_SHORTCUT_KEY,
|
|
264
|
+
(event) => {
|
|
265
|
+
if (open) {
|
|
266
|
+
event.preventDefault()
|
|
267
|
+
focusSearchInput()
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!onOpen) return
|
|
272
|
+
|
|
273
|
+
event.preventDefault()
|
|
274
|
+
onOpen()
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
enabled: enableShortcut && isVisible && canSearch,
|
|
278
|
+
enableOnContentEditable: true,
|
|
279
|
+
enableOnFormTags: true,
|
|
280
|
+
},
|
|
281
|
+
[open, onOpen, focusSearchInput]
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
// While search mode is on, plain arrow keys navigate results even when
|
|
285
|
+
// focus sits outside the panel (e.g. on the page body right after
|
|
286
|
+
// load). Form fields and the editor itself are excluded by default, so
|
|
287
|
+
// the caret and other inputs keep their arrows; inside the panel the
|
|
288
|
+
// panel's own key handler takes over.
|
|
289
|
+
useHotkeys(
|
|
290
|
+
"up,down",
|
|
291
|
+
(event) => {
|
|
292
|
+
const target = event.target instanceof Node ? event.target : null
|
|
293
|
+
if (panelRef.current?.contains(target)) return
|
|
294
|
+
|
|
295
|
+
event.preventDefault()
|
|
296
|
+
if (event.key === "ArrowDown") {
|
|
297
|
+
goToNext()
|
|
298
|
+
} else {
|
|
299
|
+
goToPrevious()
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{ enabled: open && canNavigate },
|
|
303
|
+
[goToNext, goToPrevious]
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
// Re-apply the stored terms when the panel becomes open and clear
|
|
307
|
+
// highlights (keeping the values) when it hides. Refs keep the effect
|
|
308
|
+
// from re-firing on every keystroke.
|
|
309
|
+
const applySearchRef = useRef(applySearch)
|
|
310
|
+
const suspendSearchRef = useRef(suspendSearch)
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
applySearchRef.current = applySearch
|
|
313
|
+
suspendSearchRef.current = suspendSearch
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
const previousOpenRef = useRef(false)
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (previousOpenRef.current === open) return
|
|
319
|
+
previousOpenRef.current = open
|
|
320
|
+
|
|
321
|
+
if (open) {
|
|
322
|
+
applySearchRef.current()
|
|
323
|
+
} else {
|
|
324
|
+
suspendSearchRef.current()
|
|
325
|
+
}
|
|
326
|
+
}, [open])
|
|
327
|
+
|
|
328
|
+
// Focus the search input once per open, waiting until the editor is
|
|
329
|
+
// ready — on a freshly loaded page the input is still disabled for a
|
|
330
|
+
// moment, and focusing a disabled input is a no-op.
|
|
331
|
+
const focusedForOpenRef = useRef(false)
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
if (!open) {
|
|
334
|
+
focusedForOpenRef.current = false
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
if (focusedForOpenRef.current || !autoFocusSearch || !canSearch) return
|
|
338
|
+
|
|
339
|
+
focusedForOpenRef.current = true
|
|
340
|
+
focusSearchInput()
|
|
341
|
+
}, [open, canSearch, autoFocusSearch, focusSearchInput])
|
|
342
|
+
|
|
343
|
+
const handlePanelKeyDown = useCallback(
|
|
344
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
345
|
+
if (event.key === "Escape") {
|
|
346
|
+
if (onClose) {
|
|
347
|
+
event.preventDefault()
|
|
348
|
+
event.stopPropagation()
|
|
349
|
+
onClose()
|
|
350
|
+
}
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Arrow up/down step through the results from anywhere inside the
|
|
355
|
+
// panel — inputs, nav buttons, options — like Notion's find bar.
|
|
356
|
+
// Handled at the panel so a focused button navigates too (and the
|
|
357
|
+
// page doesn't scroll instead).
|
|
358
|
+
if (
|
|
359
|
+
(event.key === "ArrowDown" || event.key === "ArrowUp") &&
|
|
360
|
+
!event.shiftKey &&
|
|
361
|
+
!event.altKey &&
|
|
362
|
+
!isModKey(event) &&
|
|
363
|
+
!event.nativeEvent.isComposing &&
|
|
364
|
+
canNavigate
|
|
365
|
+
) {
|
|
366
|
+
event.preventDefault()
|
|
367
|
+
if (event.key === "ArrowDown") {
|
|
368
|
+
goToNext()
|
|
369
|
+
} else {
|
|
370
|
+
goToPrevious()
|
|
371
|
+
}
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Navigation shortcuts are scoped to the open panel on purpose, so
|
|
376
|
+
// they never compete with shortcuts elsewhere in the app (e.g. the
|
|
377
|
+
// image download button also uses Mod+Shift+D).
|
|
378
|
+
if (isModKey(event) && event.shiftKey && !event.altKey) {
|
|
379
|
+
const key = event.key.toLowerCase()
|
|
380
|
+
|
|
381
|
+
if (key === "f" && canNavigate) {
|
|
382
|
+
event.preventDefault()
|
|
383
|
+
event.stopPropagation()
|
|
384
|
+
goToNext()
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (key === "d" && canNavigate) {
|
|
389
|
+
event.preventDefault()
|
|
390
|
+
event.stopPropagation()
|
|
391
|
+
goToPrevious()
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
[onClose, canNavigate, goToNext, goToPrevious]
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
const handleSearchKeyDown = useCallback(
|
|
399
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
400
|
+
if (
|
|
401
|
+
event.key === "Enter" &&
|
|
402
|
+
!event.shiftKey &&
|
|
403
|
+
!isModKey(event) &&
|
|
404
|
+
!event.nativeEvent.isComposing &&
|
|
405
|
+
canNavigate
|
|
406
|
+
) {
|
|
407
|
+
event.preventDefault()
|
|
408
|
+
goToNext()
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
[canNavigate, goToNext]
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
const handleReplaceKeyDown = useCallback(
|
|
415
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
416
|
+
if (
|
|
417
|
+
event.key === "Enter" &&
|
|
418
|
+
!event.shiftKey &&
|
|
419
|
+
!isModKey(event) &&
|
|
420
|
+
!event.nativeEvent.isComposing &&
|
|
421
|
+
canReplace
|
|
422
|
+
) {
|
|
423
|
+
event.preventDefault()
|
|
424
|
+
replaceCurrent()
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
[canReplace, replaceCurrent]
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if (!isVisible) {
|
|
431
|
+
return null
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<Card
|
|
436
|
+
className={cn("tiptap-search-replace", className)}
|
|
437
|
+
data-open={open ? "true" : "false"}
|
|
438
|
+
role="dialog"
|
|
439
|
+
aria-label="Search and replace"
|
|
440
|
+
onKeyDown={handlePanelKeyDown}
|
|
441
|
+
ref={composedPanelRef}
|
|
442
|
+
{...divProps}
|
|
443
|
+
>
|
|
444
|
+
<CardHeader className="tiptap-search-replace-header">
|
|
445
|
+
<span className="tiptap-search-replace-count" aria-live="polite">
|
|
446
|
+
{resultCountLabel}
|
|
447
|
+
</span>
|
|
448
|
+
|
|
449
|
+
<div className="tiptap-search-replace-nav-group">
|
|
450
|
+
<ButtonGroup className="button-group" orientation="horizontal">
|
|
451
|
+
<ButtonGroup>
|
|
452
|
+
<Button
|
|
453
|
+
type="button"
|
|
454
|
+
data-search-replace-action="prev"
|
|
455
|
+
variant="ghost"
|
|
456
|
+
size="small"
|
|
457
|
+
disabled={!canNavigate}
|
|
458
|
+
aria-label="Previous result"
|
|
459
|
+
tooltip="Previous result"
|
|
460
|
+
shortcutKeys={PREVIOUS_RESULT_SHORTCUT_KEY}
|
|
461
|
+
onClick={goToPrevious}
|
|
462
|
+
>
|
|
463
|
+
<ChevronUpIcon className="tiptap-button-icon" />
|
|
464
|
+
</Button>
|
|
465
|
+
</ButtonGroup>
|
|
466
|
+
<ButtonGroup>
|
|
467
|
+
<Button
|
|
468
|
+
type="button"
|
|
469
|
+
data-search-replace-action="next"
|
|
470
|
+
variant="ghost"
|
|
471
|
+
size="small"
|
|
472
|
+
disabled={!canNavigate}
|
|
473
|
+
aria-label="Next result"
|
|
474
|
+
tooltip="Next result"
|
|
475
|
+
shortcutKeys={NEXT_RESULT_SHORTCUT_KEY}
|
|
476
|
+
onClick={goToNext}
|
|
477
|
+
>
|
|
478
|
+
<ChevronDownIcon className="tiptap-button-icon" />
|
|
479
|
+
</Button>
|
|
480
|
+
</ButtonGroup>
|
|
481
|
+
</ButtonGroup>
|
|
482
|
+
|
|
483
|
+
<ButtonGroup className="button-group" orientation="horizontal">
|
|
484
|
+
<Button
|
|
485
|
+
type="button"
|
|
486
|
+
data-search-replace-action="close"
|
|
487
|
+
variant="ghost"
|
|
488
|
+
size="small"
|
|
489
|
+
aria-label="Close"
|
|
490
|
+
tooltip="Close"
|
|
491
|
+
shortcutKeys="esc"
|
|
492
|
+
onClick={onClose}
|
|
493
|
+
>
|
|
494
|
+
<CloseIcon className="tiptap-button-icon" />
|
|
495
|
+
</Button>
|
|
496
|
+
</ButtonGroup>
|
|
497
|
+
</div>
|
|
498
|
+
</CardHeader>
|
|
499
|
+
|
|
500
|
+
<CardBody className="tiptap-search-replace-body">
|
|
501
|
+
<div className="tiptap-search-replace-query-controls">
|
|
502
|
+
<div className="tiptap-search-replace-inputs">
|
|
503
|
+
<InputGroup className="tiptap-search-replace-input-group">
|
|
504
|
+
<InputGroupInput
|
|
505
|
+
data-field="search-query"
|
|
506
|
+
placeholder="Search"
|
|
507
|
+
aria-label="Search"
|
|
508
|
+
value={searchTerm}
|
|
509
|
+
autoFocus={open && autoFocusSearch}
|
|
510
|
+
autoComplete="off"
|
|
511
|
+
autoCorrect="off"
|
|
512
|
+
autoCapitalize="off"
|
|
513
|
+
spellCheck={false}
|
|
514
|
+
disabled={!canSearch}
|
|
515
|
+
onChange={(event) => setSearchTerm(event.target.value)}
|
|
516
|
+
onKeyDown={handleSearchKeyDown}
|
|
517
|
+
ref={searchInputRef}
|
|
518
|
+
/>
|
|
519
|
+
<InputGroupAddon
|
|
520
|
+
align="inline-end"
|
|
521
|
+
className="tiptap-search-replace-keyboard-hint-addon"
|
|
522
|
+
>
|
|
523
|
+
<kbd
|
|
524
|
+
className="tiptap-search-replace-keyboard-hint"
|
|
525
|
+
aria-hidden="true"
|
|
526
|
+
>
|
|
527
|
+
↵
|
|
528
|
+
</kbd>
|
|
529
|
+
</InputGroupAddon>
|
|
530
|
+
</InputGroup>
|
|
531
|
+
|
|
532
|
+
<InputGroup className="tiptap-search-replace-input-group">
|
|
533
|
+
<InputGroupInput
|
|
534
|
+
data-field="replace-query"
|
|
535
|
+
placeholder="Replace"
|
|
536
|
+
aria-label="Replace"
|
|
537
|
+
value={replaceTerm}
|
|
538
|
+
autoComplete="off"
|
|
539
|
+
autoCorrect="off"
|
|
540
|
+
autoCapitalize="off"
|
|
541
|
+
spellCheck={false}
|
|
542
|
+
disabled={!canSearch}
|
|
543
|
+
onChange={(event) => setReplaceTerm(event.target.value)}
|
|
544
|
+
onKeyDown={handleReplaceKeyDown}
|
|
545
|
+
/>
|
|
546
|
+
<InputGroupAddon
|
|
547
|
+
align="inline-end"
|
|
548
|
+
className="tiptap-search-replace-keyboard-hint-addon"
|
|
549
|
+
>
|
|
550
|
+
<kbd
|
|
551
|
+
className="tiptap-search-replace-keyboard-hint"
|
|
552
|
+
aria-hidden="true"
|
|
553
|
+
>
|
|
554
|
+
↵
|
|
555
|
+
</kbd>
|
|
556
|
+
</InputGroupAddon>
|
|
557
|
+
</InputGroup>
|
|
558
|
+
</div>
|
|
559
|
+
|
|
560
|
+
<div className="tiptap-search-replace-options">
|
|
561
|
+
<Button
|
|
562
|
+
type="button"
|
|
563
|
+
data-option="match-case"
|
|
564
|
+
variant="check"
|
|
565
|
+
aria-checked={caseSensitive}
|
|
566
|
+
disabled={!canSearch}
|
|
567
|
+
onClick={toggleCaseSensitive}
|
|
568
|
+
>
|
|
569
|
+
<CaseSensitiveIcon
|
|
570
|
+
className="tiptap-button-icon"
|
|
571
|
+
aria-hidden="true"
|
|
572
|
+
/>
|
|
573
|
+
<span className="tiptap-button-text">Match case</span>
|
|
574
|
+
</Button>
|
|
575
|
+
|
|
576
|
+
<Button
|
|
577
|
+
type="button"
|
|
578
|
+
data-option="whole-words"
|
|
579
|
+
variant="check"
|
|
580
|
+
aria-checked={wholeWord}
|
|
581
|
+
disabled={!canSearch}
|
|
582
|
+
onClick={toggleWholeWord}
|
|
583
|
+
>
|
|
584
|
+
<WholeWordIcon
|
|
585
|
+
className="tiptap-button-icon"
|
|
586
|
+
aria-hidden="true"
|
|
587
|
+
/>
|
|
588
|
+
<span className="tiptap-button-text">Whole words</span>
|
|
589
|
+
</Button>
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<Separator orientation="horizontal" />
|
|
594
|
+
|
|
595
|
+
<div className="tiptap-search-replace-regex-toggle">
|
|
596
|
+
<label
|
|
597
|
+
data-option="use-regex"
|
|
598
|
+
className="tiptap-search-replace-regex-toggle-row"
|
|
599
|
+
>
|
|
600
|
+
<span className="tiptap-search-replace-regex-toggle-label">
|
|
601
|
+
Use regular expression
|
|
602
|
+
</span>
|
|
603
|
+
<Switch
|
|
604
|
+
checked={useRegex}
|
|
605
|
+
onCheckedChange={toggleUseRegex}
|
|
606
|
+
disabled={!canSearch}
|
|
607
|
+
/>
|
|
608
|
+
</label>
|
|
609
|
+
|
|
610
|
+
{useRegex && (
|
|
611
|
+
<div className="tiptap-search-replace-regex-help">
|
|
612
|
+
<div className="tiptap-search-replace-regex-help-section">
|
|
613
|
+
<span className="tiptap-search-replace-regex-help-label">
|
|
614
|
+
Try a search pattern
|
|
615
|
+
</span>
|
|
616
|
+
|
|
617
|
+
<ButtonGroup
|
|
618
|
+
orientation="vertical"
|
|
619
|
+
className="tiptap-search-replace-regex-example-list"
|
|
620
|
+
>
|
|
621
|
+
{REGEX_SEARCH_EXAMPLES.map(({ label, pattern }) => (
|
|
622
|
+
<RegexExampleButton
|
|
623
|
+
key={pattern}
|
|
624
|
+
pattern={pattern}
|
|
625
|
+
onApply={() => applyRegexExample(pattern)}
|
|
626
|
+
>
|
|
627
|
+
{label} <code>{pattern}</code>
|
|
628
|
+
</RegexExampleButton>
|
|
629
|
+
))}
|
|
630
|
+
</ButtonGroup>
|
|
631
|
+
</div>
|
|
632
|
+
|
|
633
|
+
<Separator
|
|
634
|
+
orientation="horizontal"
|
|
635
|
+
className="tiptap-search-replace-regex-help-separator"
|
|
636
|
+
/>
|
|
637
|
+
|
|
638
|
+
<div className="tiptap-search-replace-regex-help-section">
|
|
639
|
+
<span className="tiptap-search-replace-regex-help-label">
|
|
640
|
+
Try search and replace
|
|
641
|
+
</span>
|
|
642
|
+
|
|
643
|
+
<ButtonGroup
|
|
644
|
+
orientation="vertical"
|
|
645
|
+
className="tiptap-search-replace-regex-example-list"
|
|
646
|
+
>
|
|
647
|
+
{REGEX_REPLACE_EXAMPLES.map(({ pattern, replacement }) => (
|
|
648
|
+
<RegexExampleButton
|
|
649
|
+
key={pattern}
|
|
650
|
+
pattern={pattern}
|
|
651
|
+
onApply={() => applyRegexExample(pattern, replacement)}
|
|
652
|
+
>
|
|
653
|
+
<code>{pattern}</code> to <code>{replacement}</code>
|
|
654
|
+
</RegexExampleButton>
|
|
655
|
+
))}
|
|
656
|
+
</ButtonGroup>
|
|
657
|
+
|
|
658
|
+
<a
|
|
659
|
+
className="tiptap-search-replace-regex-docs-link"
|
|
660
|
+
data-search-replace-action="regex-docs"
|
|
661
|
+
href={REGEX_DOCS_URL}
|
|
662
|
+
target="_blank"
|
|
663
|
+
rel="noopener noreferrer"
|
|
664
|
+
>
|
|
665
|
+
Learn about regular expressions
|
|
666
|
+
<ExternalLinkIcon
|
|
667
|
+
className="tiptap-search-replace-regex-docs-link-icon"
|
|
668
|
+
aria-hidden="true"
|
|
669
|
+
/>
|
|
670
|
+
</a>
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
)}
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
<Separator orientation="horizontal" />
|
|
677
|
+
</CardBody>
|
|
678
|
+
|
|
679
|
+
<CardFooter className="tiptap-search-replace-actions">
|
|
680
|
+
<ButtonGroup className="button-group" orientation="horizontal">
|
|
681
|
+
<ButtonGroup>
|
|
682
|
+
<Button
|
|
683
|
+
type="button"
|
|
684
|
+
data-search-replace-action="replace"
|
|
685
|
+
disabled={!canReplace}
|
|
686
|
+
aria-label="Replace current result"
|
|
687
|
+
onClick={replaceCurrent}
|
|
688
|
+
>
|
|
689
|
+
Replace
|
|
690
|
+
</Button>
|
|
691
|
+
</ButtonGroup>
|
|
692
|
+
<ButtonGroup>
|
|
693
|
+
<Button
|
|
694
|
+
type="button"
|
|
695
|
+
data-search-replace-action="replace-all"
|
|
696
|
+
disabled={!canReplaceAll}
|
|
697
|
+
aria-label="Replace all results"
|
|
698
|
+
onClick={replaceAll}
|
|
699
|
+
>
|
|
700
|
+
Replace all
|
|
701
|
+
</Button>
|
|
702
|
+
</ButtonGroup>
|
|
703
|
+
</ButtonGroup>
|
|
704
|
+
</CardFooter>
|
|
705
|
+
</Card>
|
|
706
|
+
)
|
|
707
|
+
}
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
SearchAndReplace.displayName = "SearchAndReplace"
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Alias of `SearchAndReplace` for contexts where the panel is embedded as
|
|
714
|
+
* plain content (e.g. inside a collapsed mobile toolbar).
|
|
715
|
+
*/
|
|
716
|
+
export const SearchAndReplaceContent = SearchAndReplace
|
|
717
|
+
|
|
718
|
+
export default SearchAndReplace
|