@betterstart/cli 0.1.3 → 0.1.4
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/README.md +133 -0
- package/dist/cli.d.ts +1 -9
- package/dist/cli.js +13484 -367
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +24 -266
- package/dist/index.js +4 -11378
- package/dist/index.js.map +1 -1
- package/package.json +29 -42
- package/templates/schema.json +959 -0
- package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
- package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
- package/templates/tiptap/hooks/use-element-rect.ts +166 -0
- package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
- package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
- package/templates/tiptap/hooks/use-scrolling.ts +64 -0
- package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
- package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
- package/templates/tiptap/hooks/use-unmount.ts +21 -0
- package/templates/tiptap/hooks/use-window-size.ts +87 -0
- package/templates/tiptap/lib/tiptap-utils.ts +587 -0
- package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
- package/templates/tiptap/styles/_variables.scss +296 -0
- package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
- package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
- package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
- package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
- package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
- package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
- package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
- package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
- package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
- package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
- package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
- package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
- package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
- package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
- package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
- package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
- package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
- package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
- package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
- package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
- package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
- package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
- package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
- package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
- package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
- package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
- package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
- package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
- package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
- package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
- package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
- package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
- package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
- package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
- package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
- package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
- package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
- package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
- package/templates/ui/accordion.tsx +52 -0
- package/templates/ui/alert-dialog.tsx +116 -0
- package/templates/ui/alert.tsx +48 -0
- package/templates/ui/aspect-ratio.tsx +7 -0
- package/templates/ui/avatar.tsx +46 -0
- package/templates/ui/badge.tsx +32 -0
- package/templates/ui/breadcrumb.tsx +98 -0
- package/templates/ui/button-group.tsx +77 -0
- package/templates/ui/button.tsx +48 -0
- package/templates/ui/calendar.tsx +176 -0
- package/templates/ui/card.tsx +54 -0
- package/templates/ui/carousel.tsx +234 -0
- package/templates/ui/chart.tsx +349 -0
- package/templates/ui/checkbox.tsx +27 -0
- package/templates/ui/collapsible.tsx +11 -0
- package/templates/ui/command.tsx +142 -0
- package/templates/ui/context-menu.tsx +188 -0
- package/templates/ui/curriculum-editor.tsx +601 -0
- package/templates/ui/date-picker.tsx +70 -0
- package/templates/ui/dialog.tsx +103 -0
- package/templates/ui/drawer.tsx +99 -0
- package/templates/ui/dropdown-menu.tsx +185 -0
- package/templates/ui/dynamic-list-field.tsx +95 -0
- package/templates/ui/empty.tsx +90 -0
- package/templates/ui/field.tsx +231 -0
- package/templates/ui/file-upload-example.tsx +113 -0
- package/templates/ui/form.tsx +172 -0
- package/templates/ui/hover-card.tsx +28 -0
- package/templates/ui/icon-picker.tsx +435 -0
- package/templates/ui/icons-data.ts +6 -0
- package/templates/ui/image-upload-field.tsx +360 -0
- package/templates/ui/input-group.tsx +160 -0
- package/templates/ui/input-otp.tsx +70 -0
- package/templates/ui/input.tsx +21 -0
- package/templates/ui/item.tsx +171 -0
- package/templates/ui/kbd.tsx +28 -0
- package/templates/ui/label.tsx +20 -0
- package/templates/ui/logo.tsx +113 -0
- package/templates/ui/markdown-editor.tsx +303 -0
- package/templates/ui/markdown-utils.ts +128 -0
- package/templates/ui/media-upload-field.tsx +255 -0
- package/templates/ui/menubar.tsx +230 -0
- package/templates/ui/navigation-menu.tsx +119 -0
- package/templates/ui/pagination.tsx +96 -0
- package/templates/ui/placeholder.tsx +25 -0
- package/templates/ui/popover.tsx +32 -0
- package/templates/ui/progress.tsx +24 -0
- package/templates/ui/radio-group.tsx +37 -0
- package/templates/ui/resizable.tsx +41 -0
- package/templates/ui/rich-text-editor.tsx +374 -0
- package/templates/ui/scroll-area.tsx +45 -0
- package/templates/ui/select.tsx +151 -0
- package/templates/ui/separator.tsx +25 -0
- package/templates/ui/sheet.tsx +120 -0
- package/templates/ui/sidebar.tsx +684 -0
- package/templates/ui/skeleton.tsx +7 -0
- package/templates/ui/slider.tsx +24 -0
- package/templates/ui/sonner.tsx +29 -0
- package/templates/ui/spinner.tsx +15 -0
- package/templates/ui/switch.tsx +28 -0
- package/templates/ui/table.tsx +93 -0
- package/templates/ui/tabs.tsx +54 -0
- package/templates/ui/textarea.tsx +20 -0
- package/templates/ui/toast.tsx +127 -0
- package/templates/ui/toggle-group.tsx +56 -0
- package/templates/ui/toggle.tsx +43 -0
- package/templates/ui/tooltip.tsx +31 -0
- package/templates/ui/use-mobile.tsx +19 -0
- package/templates/ui/video-upload-field.tsx +368 -0
- package/dist/chunk-EIH4RRIJ.js +0 -183
- package/dist/chunk-EIH4RRIJ.js.map +0 -1
- package/dist/chunk-NKRQYAS6.js +0 -260
- package/dist/chunk-NKRQYAS6.js.map +0 -1
- package/dist/chunk-QLVSHP7X.js +0 -235
- package/dist/chunk-QLVSHP7X.js.map +0 -1
- package/dist/chunk-WY6BC55D.js +0 -357
- package/dist/chunk-WY6BC55D.js.map +0 -1
- package/dist/config/index.d.ts +0 -93
- package/dist/config/index.js +0 -58
- package/dist/config/index.js.map +0 -1
- package/dist/core/index.d.ts +0 -415
- package/dist/core/index.js +0 -906
- package/dist/core/index.js.map +0 -1
- package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
- package/dist/logger-awLb347n.d.ts +0 -81
- package/dist/plugins/index.d.ts +0 -213
- package/dist/plugins/index.js +0 -365
- package/dist/plugins/index.js.map +0 -1
- package/dist/types-ByX_gl6y.d.ts +0 -232
- package/dist/types-eI549DEG.d.ts +0 -331
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Editor } from '@tiptap/react'
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
// --- Hooks ---
|
|
7
|
+
import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
|
|
8
|
+
// --- Lib ---
|
|
9
|
+
import { isMarkInSchema, isNodeTypeSelected, sanitizeUrl } from '../../lib/tiptap-utils'
|
|
10
|
+
// --- Icons ---
|
|
11
|
+
import { LinkIcon } from '../../tiptap-icons/link-icon'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Configuration for the link popover functionality
|
|
15
|
+
*/
|
|
16
|
+
export interface UseLinkPopoverConfig {
|
|
17
|
+
/**
|
|
18
|
+
* The Tiptap editor instance.
|
|
19
|
+
*/
|
|
20
|
+
editor?: Editor | null
|
|
21
|
+
/**
|
|
22
|
+
* Whether to hide the link popover when not available.
|
|
23
|
+
* @default false
|
|
24
|
+
*/
|
|
25
|
+
hideWhenUnavailable?: boolean
|
|
26
|
+
/**
|
|
27
|
+
* Callback function called when the link is set.
|
|
28
|
+
*/
|
|
29
|
+
onSetLink?: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Configuration for the link handler functionality
|
|
34
|
+
*/
|
|
35
|
+
export interface LinkHandlerProps {
|
|
36
|
+
/**
|
|
37
|
+
* The Tiptap editor instance.
|
|
38
|
+
*/
|
|
39
|
+
editor: Editor | null
|
|
40
|
+
/**
|
|
41
|
+
* Callback function called when the link is set.
|
|
42
|
+
*/
|
|
43
|
+
onSetLink?: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Checks if a link can be set in the current editor state
|
|
48
|
+
*/
|
|
49
|
+
export function canSetLink(editor: Editor | null): boolean {
|
|
50
|
+
if (!editor || !editor.isEditable) return false
|
|
51
|
+
|
|
52
|
+
// The third argument 'true' checks whether the current selection is inside an image caption, and prevents setting a link there
|
|
53
|
+
// If the selection is inside an image caption, we can't set a link
|
|
54
|
+
if (isNodeTypeSelected(editor, ['image'], true)) return false
|
|
55
|
+
try {
|
|
56
|
+
return editor.can().setMark('link')
|
|
57
|
+
} catch {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Checks if a link is currently active in the editor
|
|
64
|
+
*/
|
|
65
|
+
export function isLinkActive(editor: Editor | null): boolean {
|
|
66
|
+
if (!editor || !editor.isEditable) return false
|
|
67
|
+
return editor.isActive('link')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Determines if the link button should be shown
|
|
72
|
+
*/
|
|
73
|
+
export function shouldShowLinkButton(props: {
|
|
74
|
+
editor: Editor | null
|
|
75
|
+
hideWhenUnavailable: boolean
|
|
76
|
+
}): boolean {
|
|
77
|
+
const { editor, hideWhenUnavailable } = props
|
|
78
|
+
|
|
79
|
+
if (!editor || !editor.isEditable) return false
|
|
80
|
+
|
|
81
|
+
const linkInSchema = isMarkInSchema('link', editor)
|
|
82
|
+
|
|
83
|
+
// If hideWhenUnavailable is false, always show the button (even if disabled)
|
|
84
|
+
if (!hideWhenUnavailable) {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// hideWhenUnavailable is true: hide if link is not in schema
|
|
89
|
+
if (!linkInSchema) {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// hideWhenUnavailable is true: hide if we can't set a link (unless in code block)
|
|
94
|
+
if (!editor.isActive('code')) {
|
|
95
|
+
return canSetLink(editor)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Custom hook for handling link operations in a Tiptap editor
|
|
103
|
+
*/
|
|
104
|
+
export function useLinkHandler(props: LinkHandlerProps) {
|
|
105
|
+
const { editor, onSetLink } = props
|
|
106
|
+
const [url, setUrl] = useState<string | null>(null)
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!editor) return
|
|
110
|
+
|
|
111
|
+
// Get URL immediately on mount
|
|
112
|
+
const { href } = editor.getAttributes('link')
|
|
113
|
+
|
|
114
|
+
if (isLinkActive(editor) && url === null) {
|
|
115
|
+
setUrl(href || '')
|
|
116
|
+
}
|
|
117
|
+
}, [editor, url])
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!editor) return
|
|
121
|
+
|
|
122
|
+
const updateLinkState = () => {
|
|
123
|
+
const { href } = editor.getAttributes('link')
|
|
124
|
+
setUrl(href || '')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
editor.on('selectionUpdate', updateLinkState)
|
|
128
|
+
return () => {
|
|
129
|
+
editor.off('selectionUpdate', updateLinkState)
|
|
130
|
+
}
|
|
131
|
+
}, [editor])
|
|
132
|
+
|
|
133
|
+
const setLink = useCallback(() => {
|
|
134
|
+
if (!url || !editor) return
|
|
135
|
+
|
|
136
|
+
const { selection } = editor.state
|
|
137
|
+
const isEmpty = selection.empty
|
|
138
|
+
|
|
139
|
+
let chain = editor.chain().focus()
|
|
140
|
+
|
|
141
|
+
chain = chain.extendMarkRange('link').setLink({ href: url })
|
|
142
|
+
|
|
143
|
+
if (isEmpty) {
|
|
144
|
+
chain = chain.insertContent({ type: 'text', text: url })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
chain.run()
|
|
148
|
+
|
|
149
|
+
setUrl(null)
|
|
150
|
+
|
|
151
|
+
onSetLink?.()
|
|
152
|
+
}, [editor, onSetLink, url])
|
|
153
|
+
|
|
154
|
+
const removeLink = useCallback(() => {
|
|
155
|
+
if (!editor) return
|
|
156
|
+
editor
|
|
157
|
+
.chain()
|
|
158
|
+
.focus()
|
|
159
|
+
.extendMarkRange('link')
|
|
160
|
+
.unsetLink()
|
|
161
|
+
.setMeta('preventAutolink', true)
|
|
162
|
+
.run()
|
|
163
|
+
setUrl('')
|
|
164
|
+
}, [editor])
|
|
165
|
+
|
|
166
|
+
const openLink = useCallback(
|
|
167
|
+
(target: string = '_blank', features: string = 'noopener,noreferrer') => {
|
|
168
|
+
if (!url) return
|
|
169
|
+
|
|
170
|
+
const safeUrl = sanitizeUrl(url, window.location.href)
|
|
171
|
+
if (safeUrl !== '#') {
|
|
172
|
+
window.open(safeUrl, target, features)
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
[url]
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
url: url || '',
|
|
180
|
+
setUrl,
|
|
181
|
+
setLink,
|
|
182
|
+
removeLink,
|
|
183
|
+
openLink
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Custom hook for link popover state management
|
|
189
|
+
*/
|
|
190
|
+
export function useLinkState(props: { editor: Editor | null; hideWhenUnavailable: boolean }) {
|
|
191
|
+
const { editor, hideWhenUnavailable = false } = props
|
|
192
|
+
|
|
193
|
+
const canSet = canSetLink(editor)
|
|
194
|
+
const isActive = isLinkActive(editor)
|
|
195
|
+
|
|
196
|
+
const [isVisible, setIsVisible] = useState(true)
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!editor) return
|
|
200
|
+
|
|
201
|
+
const handleSelectionUpdate = () => {
|
|
202
|
+
setIsVisible(
|
|
203
|
+
shouldShowLinkButton({
|
|
204
|
+
editor,
|
|
205
|
+
hideWhenUnavailable
|
|
206
|
+
})
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
handleSelectionUpdate()
|
|
211
|
+
|
|
212
|
+
editor.on('selectionUpdate', handleSelectionUpdate)
|
|
213
|
+
|
|
214
|
+
return () => {
|
|
215
|
+
editor.off('selectionUpdate', handleSelectionUpdate)
|
|
216
|
+
}
|
|
217
|
+
}, [editor, hideWhenUnavailable])
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
isVisible,
|
|
221
|
+
canSet,
|
|
222
|
+
isActive
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Main hook that provides link popover functionality for Tiptap editor
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```tsx
|
|
231
|
+
* // Simple usage
|
|
232
|
+
* function MyLinkButton() {
|
|
233
|
+
* const { isVisible, canSet, isActive, Icon, label } = useLinkPopover()
|
|
234
|
+
*
|
|
235
|
+
* if (!isVisible) return null
|
|
236
|
+
*
|
|
237
|
+
* return <button disabled={!canSet}>Link</button>
|
|
238
|
+
* }
|
|
239
|
+
*
|
|
240
|
+
* // Advanced usage with configuration
|
|
241
|
+
* function MyAdvancedLinkButton() {
|
|
242
|
+
* const { isVisible, canSet, isActive, Icon, label } = useLinkPopover({
|
|
243
|
+
* editor: myEditor,
|
|
244
|
+
* hideWhenUnavailable: true,
|
|
245
|
+
* onSetLink: () => console.log('Link set!')
|
|
246
|
+
* })
|
|
247
|
+
*
|
|
248
|
+
* if (!isVisible) return null
|
|
249
|
+
*
|
|
250
|
+
* return (
|
|
251
|
+
* <MyButton
|
|
252
|
+
* disabled={!canSet}
|
|
253
|
+
* aria-label={label}
|
|
254
|
+
* aria-pressed={isActive}
|
|
255
|
+
* >
|
|
256
|
+
* <Icon />
|
|
257
|
+
* {label}
|
|
258
|
+
* </MyButton>
|
|
259
|
+
* )
|
|
260
|
+
* }
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
export function useLinkPopover(config?: UseLinkPopoverConfig) {
|
|
264
|
+
const { editor: providedEditor, hideWhenUnavailable = false, onSetLink } = config || {}
|
|
265
|
+
|
|
266
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
267
|
+
|
|
268
|
+
const { isVisible, canSet, isActive } = useLinkState({
|
|
269
|
+
editor,
|
|
270
|
+
hideWhenUnavailable
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const linkHandler = useLinkHandler({
|
|
274
|
+
editor,
|
|
275
|
+
onSetLink
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
isVisible,
|
|
280
|
+
canSet,
|
|
281
|
+
isActive,
|
|
282
|
+
label: 'Link',
|
|
283
|
+
Icon: LinkIcon,
|
|
284
|
+
...linkHandler
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useCallback } from 'react'
|
|
4
|
+
// --- Hooks ---
|
|
5
|
+
import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
|
|
6
|
+
// --- Lib ---
|
|
7
|
+
import { parseShortcutKeys } from '../../lib/tiptap-utils'
|
|
8
|
+
import { Badge } from '../../tiptap-ui-primitive/badge'
|
|
9
|
+
// --- UI Primitives ---
|
|
10
|
+
import type { ButtonProps } from '../../tiptap-ui-primitive/button'
|
|
11
|
+
import { Button } from '../../tiptap-ui-primitive/button'
|
|
12
|
+
|
|
13
|
+
// --- Tiptap UI ---
|
|
14
|
+
import type { ListType, UseListConfig } from '.'
|
|
15
|
+
import { LIST_SHORTCUT_KEYS, useList } from '.'
|
|
16
|
+
|
|
17
|
+
export interface ListButtonProps extends Omit<ButtonProps, 'type'>, UseListConfig {
|
|
18
|
+
/**
|
|
19
|
+
* Optional text to display alongside the icon.
|
|
20
|
+
*/
|
|
21
|
+
text?: string
|
|
22
|
+
/**
|
|
23
|
+
* Optional show shortcut keys in the button.
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
showShortcut?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ListShortcutBadge({
|
|
30
|
+
type,
|
|
31
|
+
shortcutKeys = LIST_SHORTCUT_KEYS[type]
|
|
32
|
+
}: {
|
|
33
|
+
type: ListType
|
|
34
|
+
shortcutKeys?: string
|
|
35
|
+
}) {
|
|
36
|
+
return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Button component for toggling lists in a Tiptap editor.
|
|
41
|
+
*
|
|
42
|
+
* For custom button implementations, use the `useList` hook instead.
|
|
43
|
+
*/
|
|
44
|
+
export const ListButton = forwardRef<HTMLButtonElement, ListButtonProps>(
|
|
45
|
+
(
|
|
46
|
+
{
|
|
47
|
+
editor: providedEditor,
|
|
48
|
+
type,
|
|
49
|
+
text,
|
|
50
|
+
hideWhenUnavailable = false,
|
|
51
|
+
onToggled,
|
|
52
|
+
showShortcut = false,
|
|
53
|
+
onClick,
|
|
54
|
+
children,
|
|
55
|
+
...buttonProps
|
|
56
|
+
},
|
|
57
|
+
ref
|
|
58
|
+
) => {
|
|
59
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
60
|
+
const { isVisible, canToggle, isActive, handleToggle, label, shortcutKeys, Icon } = useList({
|
|
61
|
+
editor,
|
|
62
|
+
type,
|
|
63
|
+
hideWhenUnavailable,
|
|
64
|
+
onToggled
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const handleClick = useCallback(
|
|
68
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
69
|
+
onClick?.(event)
|
|
70
|
+
if (event.defaultPrevented) return
|
|
71
|
+
handleToggle()
|
|
72
|
+
},
|
|
73
|
+
[handleToggle, onClick]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if (!isVisible) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Button
|
|
82
|
+
type="button"
|
|
83
|
+
variant="ghost"
|
|
84
|
+
data-active-state={isActive ? 'on' : 'off'}
|
|
85
|
+
role="button"
|
|
86
|
+
tabIndex={-1}
|
|
87
|
+
disabled={!canToggle}
|
|
88
|
+
data-disabled={!canToggle}
|
|
89
|
+
aria-label={label}
|
|
90
|
+
aria-pressed={isActive}
|
|
91
|
+
tooltip={label}
|
|
92
|
+
onClick={handleClick}
|
|
93
|
+
{...buttonProps}
|
|
94
|
+
ref={ref}
|
|
95
|
+
>
|
|
96
|
+
{children ?? (
|
|
97
|
+
<>
|
|
98
|
+
<Icon className="tiptap-button-icon" />
|
|
99
|
+
{text && <span className="tiptap-button-text">{text}</span>}
|
|
100
|
+
{showShortcut && <ListShortcutBadge type={type} shortcutKeys={shortcutKeys} />}
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</Button>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
ListButton.displayName = 'ListButton'
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { NodeSelection, TextSelection } from '@tiptap/pm/state'
|
|
4
|
+
import type { Editor } from '@tiptap/react'
|
|
5
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
6
|
+
|
|
7
|
+
// --- Hooks ---
|
|
8
|
+
import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
|
|
9
|
+
// --- Lib ---
|
|
10
|
+
import {
|
|
11
|
+
findNodePosition,
|
|
12
|
+
getSelectedBlockNodes,
|
|
13
|
+
isNodeInSchema,
|
|
14
|
+
isNodeTypeSelected,
|
|
15
|
+
isValidPosition,
|
|
16
|
+
selectionWithinConvertibleTypes
|
|
17
|
+
} from '../../lib/tiptap-utils'
|
|
18
|
+
// --- Icons ---
|
|
19
|
+
import { ListIcon } from '../../tiptap-icons/list-icon'
|
|
20
|
+
import { ListOrderedIcon } from '../../tiptap-icons/list-ordered-icon'
|
|
21
|
+
import { ListTodoIcon } from '../../tiptap-icons/list-todo-icon'
|
|
22
|
+
|
|
23
|
+
export type ListType = 'bulletList' | 'orderedList' | 'taskList'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Configuration for the list functionality
|
|
27
|
+
*/
|
|
28
|
+
export interface UseListConfig {
|
|
29
|
+
/**
|
|
30
|
+
* The Tiptap editor instance.
|
|
31
|
+
*/
|
|
32
|
+
editor?: Editor | null
|
|
33
|
+
/**
|
|
34
|
+
* The type of list to toggle.
|
|
35
|
+
*/
|
|
36
|
+
type: ListType
|
|
37
|
+
/**
|
|
38
|
+
* Whether the button should hide when list is not available.
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
hideWhenUnavailable?: boolean
|
|
42
|
+
/**
|
|
43
|
+
* Callback function called after a successful toggle.
|
|
44
|
+
*/
|
|
45
|
+
onToggled?: () => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const listIcons = {
|
|
49
|
+
bulletList: ListIcon,
|
|
50
|
+
orderedList: ListOrderedIcon,
|
|
51
|
+
taskList: ListTodoIcon
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const listLabels: Record<ListType, string> = {
|
|
55
|
+
bulletList: 'Bullet List',
|
|
56
|
+
orderedList: 'Ordered List',
|
|
57
|
+
taskList: 'Task List'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const LIST_SHORTCUT_KEYS: Record<ListType, string> = {
|
|
61
|
+
bulletList: 'mod+shift+8',
|
|
62
|
+
orderedList: 'mod+shift+7',
|
|
63
|
+
taskList: 'mod+shift+9'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks if a list can be toggled in the current editor state
|
|
68
|
+
*/
|
|
69
|
+
export function canToggleList(
|
|
70
|
+
editor: Editor | null,
|
|
71
|
+
type: ListType,
|
|
72
|
+
turnInto: boolean = true
|
|
73
|
+
): boolean {
|
|
74
|
+
if (!editor || !editor.isEditable) return false
|
|
75
|
+
if (!isNodeInSchema(type, editor) || isNodeTypeSelected(editor, ['image'])) return false
|
|
76
|
+
|
|
77
|
+
if (!turnInto) {
|
|
78
|
+
switch (type) {
|
|
79
|
+
case 'bulletList':
|
|
80
|
+
return editor.can().toggleBulletList()
|
|
81
|
+
case 'orderedList':
|
|
82
|
+
return editor.can().toggleOrderedList()
|
|
83
|
+
case 'taskList':
|
|
84
|
+
return editor.can().toggleList('taskList', 'taskItem')
|
|
85
|
+
default:
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Ensure selection is in nodes we're allowed to convert
|
|
91
|
+
if (
|
|
92
|
+
!selectionWithinConvertibleTypes(editor, [
|
|
93
|
+
'paragraph',
|
|
94
|
+
'heading',
|
|
95
|
+
'bulletList',
|
|
96
|
+
'orderedList',
|
|
97
|
+
'taskList',
|
|
98
|
+
'blockquote',
|
|
99
|
+
'codeBlock'
|
|
100
|
+
])
|
|
101
|
+
)
|
|
102
|
+
return false
|
|
103
|
+
|
|
104
|
+
// Either we can set list directly on the selection,
|
|
105
|
+
// or we can clear formatting/nodes to arrive at a list.
|
|
106
|
+
switch (type) {
|
|
107
|
+
case 'bulletList':
|
|
108
|
+
return editor.can().toggleBulletList() || editor.can().clearNodes()
|
|
109
|
+
case 'orderedList':
|
|
110
|
+
return editor.can().toggleOrderedList() || editor.can().clearNodes()
|
|
111
|
+
case 'taskList':
|
|
112
|
+
return editor.can().toggleList('taskList', 'taskItem') || editor.can().clearNodes()
|
|
113
|
+
default:
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Checks if list is currently active
|
|
120
|
+
*/
|
|
121
|
+
export function isListActive(editor: Editor | null, type: ListType): boolean {
|
|
122
|
+
if (!editor || !editor.isEditable) return false
|
|
123
|
+
|
|
124
|
+
switch (type) {
|
|
125
|
+
case 'bulletList':
|
|
126
|
+
return editor.isActive('bulletList')
|
|
127
|
+
case 'orderedList':
|
|
128
|
+
return editor.isActive('orderedList')
|
|
129
|
+
case 'taskList':
|
|
130
|
+
return editor.isActive('taskList')
|
|
131
|
+
default:
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Toggles list in the editor
|
|
138
|
+
*/
|
|
139
|
+
export function toggleList(editor: Editor | null, type: ListType): boolean {
|
|
140
|
+
if (!editor || !editor.isEditable) return false
|
|
141
|
+
if (!canToggleList(editor, type)) return false
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const view = editor.view
|
|
145
|
+
let state = view.state
|
|
146
|
+
let tr = state.tr
|
|
147
|
+
|
|
148
|
+
const blocks = getSelectedBlockNodes(editor)
|
|
149
|
+
|
|
150
|
+
// In case a selection contains multiple blocks, we only allow
|
|
151
|
+
// toggling to nide if there's exactly one block selected
|
|
152
|
+
// we also dont block the canToggle since it will fall back to the bottom logic
|
|
153
|
+
const isPossibleToTurnInto =
|
|
154
|
+
selectionWithinConvertibleTypes(editor, [
|
|
155
|
+
'paragraph',
|
|
156
|
+
'heading',
|
|
157
|
+
'bulletList',
|
|
158
|
+
'orderedList',
|
|
159
|
+
'taskList',
|
|
160
|
+
'blockquote',
|
|
161
|
+
'codeBlock'
|
|
162
|
+
]) && blocks.length === 1
|
|
163
|
+
|
|
164
|
+
// No selection, find the the cursor position
|
|
165
|
+
if (
|
|
166
|
+
(state.selection.empty || state.selection instanceof TextSelection) &&
|
|
167
|
+
isPossibleToTurnInto
|
|
168
|
+
) {
|
|
169
|
+
const pos = findNodePosition({
|
|
170
|
+
editor,
|
|
171
|
+
node: state.selection.$anchor.node(1)
|
|
172
|
+
})?.pos
|
|
173
|
+
if (!isValidPosition(pos)) return false
|
|
174
|
+
|
|
175
|
+
tr = tr.setSelection(NodeSelection.create(state.doc, pos))
|
|
176
|
+
view.dispatch(tr)
|
|
177
|
+
state = view.state
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const selection = state.selection
|
|
181
|
+
|
|
182
|
+
let chain = editor.chain().focus()
|
|
183
|
+
|
|
184
|
+
// Handle NodeSelection
|
|
185
|
+
if (selection instanceof NodeSelection) {
|
|
186
|
+
const firstChild = selection.node.firstChild?.firstChild
|
|
187
|
+
const lastChild = selection.node.lastChild?.lastChild
|
|
188
|
+
|
|
189
|
+
const from = firstChild ? selection.from + firstChild.nodeSize : selection.from + 1
|
|
190
|
+
|
|
191
|
+
const to = lastChild ? selection.to - lastChild.nodeSize : selection.to - 1
|
|
192
|
+
|
|
193
|
+
const resolvedFrom = state.doc.resolve(from)
|
|
194
|
+
const resolvedTo = state.doc.resolve(to)
|
|
195
|
+
|
|
196
|
+
chain = chain.setTextSelection(TextSelection.between(resolvedFrom, resolvedTo)).clearNodes()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (editor.isActive(type)) {
|
|
200
|
+
// Unwrap list
|
|
201
|
+
chain.liftListItem('listItem').lift('bulletList').lift('orderedList').lift('taskList').run()
|
|
202
|
+
} else {
|
|
203
|
+
// Wrap in specific list type
|
|
204
|
+
const toggleMap: Record<ListType, () => typeof chain> = {
|
|
205
|
+
bulletList: () => chain.toggleBulletList(),
|
|
206
|
+
orderedList: () => chain.toggleOrderedList(),
|
|
207
|
+
taskList: () => chain.toggleList('taskList', 'taskItem')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const toggle = toggleMap[type]
|
|
211
|
+
if (!toggle) return false
|
|
212
|
+
|
|
213
|
+
toggle().run()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
editor.chain().focus().selectTextblockEnd().run()
|
|
217
|
+
|
|
218
|
+
return true
|
|
219
|
+
} catch {
|
|
220
|
+
return false
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Determines if the list button should be shown
|
|
226
|
+
*/
|
|
227
|
+
export function shouldShowButton(props: {
|
|
228
|
+
editor: Editor | null
|
|
229
|
+
type: ListType
|
|
230
|
+
hideWhenUnavailable: boolean
|
|
231
|
+
}): boolean {
|
|
232
|
+
const { editor, type, hideWhenUnavailable } = props
|
|
233
|
+
|
|
234
|
+
if (!editor || !editor.isEditable) return false
|
|
235
|
+
|
|
236
|
+
if (!hideWhenUnavailable) {
|
|
237
|
+
return true
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!isNodeInSchema(type, editor)) return false
|
|
241
|
+
|
|
242
|
+
if (!editor.isActive('code')) {
|
|
243
|
+
return canToggleList(editor, type)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Custom hook that provides list functionality for Tiptap editor
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```tsx
|
|
254
|
+
* // Simple usage
|
|
255
|
+
* function MySimpleListButton() {
|
|
256
|
+
* const { isVisible, handleToggle, isActive } = useList({ type: "bulletList" })
|
|
257
|
+
*
|
|
258
|
+
* if (!isVisible) return null
|
|
259
|
+
*
|
|
260
|
+
* return <button onClick={handleToggle}>Bullet List</button>
|
|
261
|
+
* }
|
|
262
|
+
*
|
|
263
|
+
* // Advanced usage with configuration
|
|
264
|
+
* function MyAdvancedListButton() {
|
|
265
|
+
* const { isVisible, handleToggle, label, isActive } = useList({
|
|
266
|
+
* type: "orderedList",
|
|
267
|
+
* editor: myEditor,
|
|
268
|
+
* hideWhenUnavailable: true,
|
|
269
|
+
* onToggled: () => console.log('List toggled!')
|
|
270
|
+
* })
|
|
271
|
+
*
|
|
272
|
+
* if (!isVisible) return null
|
|
273
|
+
*
|
|
274
|
+
* return (
|
|
275
|
+
* <MyButton
|
|
276
|
+
* onClick={handleToggle}
|
|
277
|
+
* aria-label={label}
|
|
278
|
+
* aria-pressed={isActive}
|
|
279
|
+
* >
|
|
280
|
+
* Toggle List
|
|
281
|
+
* </MyButton>
|
|
282
|
+
* )
|
|
283
|
+
* }
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
export function useList(config: UseListConfig) {
|
|
287
|
+
const { editor: providedEditor, type, hideWhenUnavailable = false, onToggled } = config
|
|
288
|
+
|
|
289
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
290
|
+
const [isVisible, setIsVisible] = useState<boolean>(true)
|
|
291
|
+
const canToggle = canToggleList(editor, type)
|
|
292
|
+
const isActive = isListActive(editor, type)
|
|
293
|
+
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (!editor) return
|
|
296
|
+
|
|
297
|
+
const handleSelectionUpdate = () => {
|
|
298
|
+
setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable }))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
handleSelectionUpdate()
|
|
302
|
+
|
|
303
|
+
editor.on('selectionUpdate', handleSelectionUpdate)
|
|
304
|
+
|
|
305
|
+
return () => {
|
|
306
|
+
editor.off('selectionUpdate', handleSelectionUpdate)
|
|
307
|
+
}
|
|
308
|
+
}, [editor, type, hideWhenUnavailable])
|
|
309
|
+
|
|
310
|
+
const handleToggle = useCallback(() => {
|
|
311
|
+
if (!editor) return false
|
|
312
|
+
|
|
313
|
+
const success = toggleList(editor, type)
|
|
314
|
+
if (success) {
|
|
315
|
+
onToggled?.()
|
|
316
|
+
}
|
|
317
|
+
return success
|
|
318
|
+
}, [editor, type, onToggled])
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
isVisible,
|
|
322
|
+
isActive,
|
|
323
|
+
handleToggle,
|
|
324
|
+
canToggle,
|
|
325
|
+
label: listLabels[type],
|
|
326
|
+
shortcutKeys: LIST_SHORTCUT_KEYS[type],
|
|
327
|
+
Icon: listIcons[type]
|
|
328
|
+
}
|
|
329
|
+
}
|