@betterstart/cli 0.1.2 → 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 -354
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +24 -260
- package/dist/index.js +4 -11373
- 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-G4KI4DVB.js +0 -179
- package/dist/chunk-G4KI4DVB.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,107 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useCallback } from 'react'
|
|
4
|
+
import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
|
|
5
|
+
// --- Lib ---
|
|
6
|
+
import { parseShortcutKeys } from '../../lib/tiptap-utils'
|
|
7
|
+
import { Badge } from '../../tiptap-ui-primitive/badge'
|
|
8
|
+
|
|
9
|
+
// --- UI Primitives ---
|
|
10
|
+
import type { ButtonProps } from '../../tiptap-ui-primitive/button'
|
|
11
|
+
import { Button } from '../../tiptap-ui-primitive/button'
|
|
12
|
+
// --- Tiptap UI ---
|
|
13
|
+
import type { Level, UseHeadingConfig } from '.'
|
|
14
|
+
import { HEADING_SHORTCUT_KEYS, useHeading } from '.'
|
|
15
|
+
|
|
16
|
+
export interface HeadingButtonProps extends Omit<ButtonProps, 'type'>, UseHeadingConfig {
|
|
17
|
+
/**
|
|
18
|
+
* Optional text to display alongside the icon.
|
|
19
|
+
*/
|
|
20
|
+
text?: string
|
|
21
|
+
/**
|
|
22
|
+
* Optional show shortcut keys in the button.
|
|
23
|
+
* @default false
|
|
24
|
+
*/
|
|
25
|
+
showShortcut?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function HeadingShortcutBadge({
|
|
29
|
+
level,
|
|
30
|
+
shortcutKeys = HEADING_SHORTCUT_KEYS[level]
|
|
31
|
+
}: {
|
|
32
|
+
level: Level
|
|
33
|
+
shortcutKeys?: string
|
|
34
|
+
}) {
|
|
35
|
+
return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Button component for toggling heading in a Tiptap editor.
|
|
40
|
+
*
|
|
41
|
+
* For custom button implementations, use the `useHeading` hook instead.
|
|
42
|
+
*/
|
|
43
|
+
export const HeadingButton = forwardRef<HTMLButtonElement, HeadingButtonProps>(
|
|
44
|
+
(
|
|
45
|
+
{
|
|
46
|
+
editor: providedEditor,
|
|
47
|
+
level,
|
|
48
|
+
text,
|
|
49
|
+
hideWhenUnavailable = false,
|
|
50
|
+
onToggled,
|
|
51
|
+
showShortcut = false,
|
|
52
|
+
onClick,
|
|
53
|
+
children,
|
|
54
|
+
...buttonProps
|
|
55
|
+
},
|
|
56
|
+
ref
|
|
57
|
+
) => {
|
|
58
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
59
|
+
const { isVisible, canToggle, isActive, handleToggle, label, Icon, shortcutKeys } = useHeading({
|
|
60
|
+
editor,
|
|
61
|
+
level,
|
|
62
|
+
hideWhenUnavailable,
|
|
63
|
+
onToggled
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const handleClick = useCallback(
|
|
67
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
68
|
+
onClick?.(event)
|
|
69
|
+
if (event.defaultPrevented) return
|
|
70
|
+
handleToggle()
|
|
71
|
+
},
|
|
72
|
+
[handleToggle, onClick]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (!isVisible) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Button
|
|
81
|
+
type="button"
|
|
82
|
+
variant="ghost"
|
|
83
|
+
data-active-state={isActive ? 'on' : 'off'}
|
|
84
|
+
role="button"
|
|
85
|
+
tabIndex={-1}
|
|
86
|
+
disabled={!canToggle}
|
|
87
|
+
data-disabled={!canToggle}
|
|
88
|
+
aria-label={label}
|
|
89
|
+
aria-pressed={isActive}
|
|
90
|
+
tooltip={label}
|
|
91
|
+
onClick={handleClick}
|
|
92
|
+
{...buttonProps}
|
|
93
|
+
ref={ref}
|
|
94
|
+
>
|
|
95
|
+
{children ?? (
|
|
96
|
+
<>
|
|
97
|
+
<Icon className="tiptap-button-icon" />
|
|
98
|
+
{text && <span className="tiptap-button-text">{text}</span>}
|
|
99
|
+
{showShortcut && <HeadingShortcutBadge level={level} shortcutKeys={shortcutKeys} />}
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
</Button>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
HeadingButton.displayName = 'HeadingButton'
|
|
@@ -0,0 +1,314 @@
|
|
|
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
|
+
|
|
10
|
+
// --- Lib ---
|
|
11
|
+
import {
|
|
12
|
+
findNodePosition,
|
|
13
|
+
getSelectedBlockNodes,
|
|
14
|
+
isNodeInSchema,
|
|
15
|
+
isNodeTypeSelected,
|
|
16
|
+
isValidPosition,
|
|
17
|
+
selectionWithinConvertibleTypes
|
|
18
|
+
} from '../../lib/tiptap-utils'
|
|
19
|
+
import { HeadingFiveIcon } from '../../tiptap-icons/heading-five-icon'
|
|
20
|
+
import { HeadingFourIcon } from '../../tiptap-icons/heading-four-icon'
|
|
21
|
+
// --- Icons ---
|
|
22
|
+
import { HeadingOneIcon } from '../../tiptap-icons/heading-one-icon'
|
|
23
|
+
import { HeadingSixIcon } from '../../tiptap-icons/heading-six-icon'
|
|
24
|
+
import { HeadingThreeIcon } from '../../tiptap-icons/heading-three-icon'
|
|
25
|
+
import { HeadingTwoIcon } from '../../tiptap-icons/heading-two-icon'
|
|
26
|
+
|
|
27
|
+
export type Level = 1 | 2 | 3 | 4 | 5 | 6
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration for the heading functionality
|
|
31
|
+
*/
|
|
32
|
+
export interface UseHeadingConfig {
|
|
33
|
+
/**
|
|
34
|
+
* The Tiptap editor instance.
|
|
35
|
+
*/
|
|
36
|
+
editor?: Editor | null
|
|
37
|
+
/**
|
|
38
|
+
* The heading level.
|
|
39
|
+
*/
|
|
40
|
+
level: Level
|
|
41
|
+
/**
|
|
42
|
+
* Whether the button should hide when heading is not available.
|
|
43
|
+
* @default false
|
|
44
|
+
*/
|
|
45
|
+
hideWhenUnavailable?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* Callback function called after a successful heading toggle.
|
|
48
|
+
*/
|
|
49
|
+
onToggled?: () => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const headingIcons = {
|
|
53
|
+
1: HeadingOneIcon,
|
|
54
|
+
2: HeadingTwoIcon,
|
|
55
|
+
3: HeadingThreeIcon,
|
|
56
|
+
4: HeadingFourIcon,
|
|
57
|
+
5: HeadingFiveIcon,
|
|
58
|
+
6: HeadingSixIcon
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const HEADING_SHORTCUT_KEYS: Record<Level, string> = {
|
|
62
|
+
1: 'ctrl+alt+1',
|
|
63
|
+
2: 'ctrl+alt+2',
|
|
64
|
+
3: 'ctrl+alt+3',
|
|
65
|
+
4: 'ctrl+alt+4',
|
|
66
|
+
5: 'ctrl+alt+5',
|
|
67
|
+
6: 'ctrl+alt+6'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Checks if heading can be toggled in the current editor state
|
|
72
|
+
*/
|
|
73
|
+
export function canToggle(editor: Editor | null, level?: Level, turnInto: boolean = true): boolean {
|
|
74
|
+
if (!editor || !editor.isEditable) return false
|
|
75
|
+
if (!isNodeInSchema('heading', editor) || isNodeTypeSelected(editor, ['image'])) return false
|
|
76
|
+
|
|
77
|
+
if (!turnInto) {
|
|
78
|
+
return level ? editor.can().setNode('heading', { level }) : editor.can().setNode('heading')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Ensure selection is in nodes we're allowed to convert
|
|
82
|
+
if (
|
|
83
|
+
!selectionWithinConvertibleTypes(editor, [
|
|
84
|
+
'paragraph',
|
|
85
|
+
'heading',
|
|
86
|
+
'bulletList',
|
|
87
|
+
'orderedList',
|
|
88
|
+
'taskList',
|
|
89
|
+
'blockquote',
|
|
90
|
+
'codeBlock'
|
|
91
|
+
])
|
|
92
|
+
)
|
|
93
|
+
return false
|
|
94
|
+
|
|
95
|
+
// Either we can set heading directly on the selection,
|
|
96
|
+
// or we can clear formatting/nodes to arrive at a heading.
|
|
97
|
+
return level
|
|
98
|
+
? editor.can().setNode('heading', { level }) || editor.can().clearNodes()
|
|
99
|
+
: editor.can().setNode('heading') || editor.can().clearNodes()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Checks if heading is currently active
|
|
104
|
+
*/
|
|
105
|
+
export function isHeadingActive(editor: Editor | null, level?: Level | Level[]): boolean {
|
|
106
|
+
if (!editor || !editor.isEditable) return false
|
|
107
|
+
|
|
108
|
+
if (Array.isArray(level)) {
|
|
109
|
+
return level.some((l) => editor.isActive('heading', { level: l }))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return level ? editor.isActive('heading', { level }) : editor.isActive('heading')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Toggles heading in the editor
|
|
117
|
+
*/
|
|
118
|
+
export function toggleHeading(editor: Editor | null, level: Level | Level[]): boolean {
|
|
119
|
+
if (!editor || !editor.isEditable) return false
|
|
120
|
+
|
|
121
|
+
const levels = Array.isArray(level) ? level : [level]
|
|
122
|
+
const toggleLevel = levels.find((l) => canToggle(editor, l))
|
|
123
|
+
|
|
124
|
+
if (!toggleLevel) return false
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const view = editor.view
|
|
128
|
+
let state = view.state
|
|
129
|
+
let tr = state.tr
|
|
130
|
+
|
|
131
|
+
const blocks = getSelectedBlockNodes(editor)
|
|
132
|
+
|
|
133
|
+
// In case a selection contains multiple blocks, we only allow
|
|
134
|
+
// toggling to nide if there's exactly one block selected
|
|
135
|
+
// we also dont block the canToggle since it will fall back to the bottom logic
|
|
136
|
+
const isPossibleToTurnInto =
|
|
137
|
+
selectionWithinConvertibleTypes(editor, [
|
|
138
|
+
'paragraph',
|
|
139
|
+
'heading',
|
|
140
|
+
'bulletList',
|
|
141
|
+
'orderedList',
|
|
142
|
+
'taskList',
|
|
143
|
+
'blockquote',
|
|
144
|
+
'codeBlock'
|
|
145
|
+
]) && blocks.length === 1
|
|
146
|
+
|
|
147
|
+
// No selection, find the the cursor position
|
|
148
|
+
if (
|
|
149
|
+
(state.selection.empty || state.selection instanceof TextSelection) &&
|
|
150
|
+
isPossibleToTurnInto
|
|
151
|
+
) {
|
|
152
|
+
const pos = findNodePosition({
|
|
153
|
+
editor,
|
|
154
|
+
node: state.selection.$anchor.node(1)
|
|
155
|
+
})?.pos
|
|
156
|
+
if (!isValidPosition(pos)) return false
|
|
157
|
+
|
|
158
|
+
tr = tr.setSelection(NodeSelection.create(state.doc, pos))
|
|
159
|
+
view.dispatch(tr)
|
|
160
|
+
state = view.state
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const selection = state.selection
|
|
164
|
+
let chain = editor.chain().focus()
|
|
165
|
+
|
|
166
|
+
// Handle NodeSelection
|
|
167
|
+
if (selection instanceof NodeSelection) {
|
|
168
|
+
const firstChild = selection.node.firstChild?.firstChild
|
|
169
|
+
const lastChild = selection.node.lastChild?.lastChild
|
|
170
|
+
|
|
171
|
+
const from = firstChild ? selection.from + firstChild.nodeSize : selection.from + 1
|
|
172
|
+
|
|
173
|
+
const to = lastChild ? selection.to - lastChild.nodeSize : selection.to - 1
|
|
174
|
+
|
|
175
|
+
const resolvedFrom = state.doc.resolve(from)
|
|
176
|
+
const resolvedTo = state.doc.resolve(to)
|
|
177
|
+
|
|
178
|
+
chain = chain.setTextSelection(TextSelection.between(resolvedFrom, resolvedTo)).clearNodes()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const isActive = levels.some((l) => editor.isActive('heading', { level: l }))
|
|
182
|
+
|
|
183
|
+
const toggle = isActive
|
|
184
|
+
? chain.setNode('paragraph')
|
|
185
|
+
: chain.setNode('heading', { level: toggleLevel })
|
|
186
|
+
|
|
187
|
+
toggle.run()
|
|
188
|
+
|
|
189
|
+
editor.chain().focus().selectTextblockEnd().run()
|
|
190
|
+
|
|
191
|
+
return true
|
|
192
|
+
} catch {
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Determines if the heading button should be shown
|
|
199
|
+
*/
|
|
200
|
+
export function shouldShowButton(props: {
|
|
201
|
+
editor: Editor | null
|
|
202
|
+
level?: Level | Level[]
|
|
203
|
+
hideWhenUnavailable: boolean
|
|
204
|
+
}): boolean {
|
|
205
|
+
const { editor, level, hideWhenUnavailable } = props
|
|
206
|
+
|
|
207
|
+
if (!editor || !editor.isEditable) return false
|
|
208
|
+
|
|
209
|
+
if (!hideWhenUnavailable) {
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!isNodeInSchema('heading', editor)) return false
|
|
214
|
+
|
|
215
|
+
if (!editor.isActive('code')) {
|
|
216
|
+
if (Array.isArray(level)) {
|
|
217
|
+
return level.some((l) => canToggle(editor, l))
|
|
218
|
+
}
|
|
219
|
+
return canToggle(editor, level)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return true
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Custom hook that provides heading functionality for Tiptap editor
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```tsx
|
|
230
|
+
* // Simple usage
|
|
231
|
+
* function MySimpleHeadingButton() {
|
|
232
|
+
* const { isVisible, isActive, handleToggle, Icon } = useHeading({ level: 1 })
|
|
233
|
+
*
|
|
234
|
+
* if (!isVisible) return null
|
|
235
|
+
*
|
|
236
|
+
* return (
|
|
237
|
+
* <button
|
|
238
|
+
* onClick={handleToggle}
|
|
239
|
+
* aria-pressed={isActive}
|
|
240
|
+
* >
|
|
241
|
+
* <Icon />
|
|
242
|
+
* Heading 1
|
|
243
|
+
* </button>
|
|
244
|
+
* )
|
|
245
|
+
* }
|
|
246
|
+
*
|
|
247
|
+
* // Advanced usage with configuration
|
|
248
|
+
* function MyAdvancedHeadingButton() {
|
|
249
|
+
* const { isVisible, isActive, handleToggle, label, Icon } = useHeading({
|
|
250
|
+
* level: 2,
|
|
251
|
+
* editor: myEditor,
|
|
252
|
+
* hideWhenUnavailable: true,
|
|
253
|
+
* onToggled: (isActive) => console.log('Heading toggled:', isActive)
|
|
254
|
+
* })
|
|
255
|
+
*
|
|
256
|
+
* if (!isVisible) return null
|
|
257
|
+
*
|
|
258
|
+
* return (
|
|
259
|
+
* <MyButton
|
|
260
|
+
* onClick={handleToggle}
|
|
261
|
+
* aria-label={label}
|
|
262
|
+
* aria-pressed={isActive}
|
|
263
|
+
* >
|
|
264
|
+
* <Icon />
|
|
265
|
+
* Toggle Heading 2
|
|
266
|
+
* </MyButton>
|
|
267
|
+
* )
|
|
268
|
+
* }
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
export function useHeading(config: UseHeadingConfig) {
|
|
272
|
+
const { editor: providedEditor, level, hideWhenUnavailable = false, onToggled } = config
|
|
273
|
+
|
|
274
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
275
|
+
const [isVisible, setIsVisible] = useState<boolean>(true)
|
|
276
|
+
const canToggleState = canToggle(editor, level)
|
|
277
|
+
const isActive = isHeadingActive(editor, level)
|
|
278
|
+
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (!editor) return
|
|
281
|
+
|
|
282
|
+
const handleSelectionUpdate = () => {
|
|
283
|
+
setIsVisible(shouldShowButton({ editor, level, hideWhenUnavailable }))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
handleSelectionUpdate()
|
|
287
|
+
|
|
288
|
+
editor.on('selectionUpdate', handleSelectionUpdate)
|
|
289
|
+
|
|
290
|
+
return () => {
|
|
291
|
+
editor.off('selectionUpdate', handleSelectionUpdate)
|
|
292
|
+
}
|
|
293
|
+
}, [editor, level, hideWhenUnavailable])
|
|
294
|
+
|
|
295
|
+
const handleToggle = useCallback(() => {
|
|
296
|
+
if (!editor) return false
|
|
297
|
+
|
|
298
|
+
const success = toggleHeading(editor, level)
|
|
299
|
+
if (success) {
|
|
300
|
+
onToggled?.()
|
|
301
|
+
}
|
|
302
|
+
return success
|
|
303
|
+
}, [editor, level, onToggled])
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
isVisible,
|
|
307
|
+
isActive,
|
|
308
|
+
handleToggle,
|
|
309
|
+
canToggle: canToggleState,
|
|
310
|
+
label: `Heading ${level}`,
|
|
311
|
+
shortcutKeys: HEADING_SHORTCUT_KEYS[level],
|
|
312
|
+
Icon: headingIcons[level]
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useCallback, useState } from 'react'
|
|
4
|
+
// --- Hooks ---
|
|
5
|
+
import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
|
|
6
|
+
// --- Icons ---
|
|
7
|
+
import { ChevronDownIcon } from '../../tiptap-icons/chevron-down-icon'
|
|
8
|
+
// --- UI Primitives ---
|
|
9
|
+
import type { ButtonProps } from '../../tiptap-ui-primitive/button'
|
|
10
|
+
import { Button, ButtonGroup } from '../../tiptap-ui-primitive/button'
|
|
11
|
+
import { Card, CardBody } from '../../tiptap-ui-primitive/card'
|
|
12
|
+
import {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuContent,
|
|
15
|
+
DropdownMenuItem,
|
|
16
|
+
DropdownMenuTrigger
|
|
17
|
+
} from '../../tiptap-ui-primitive/dropdown-menu'
|
|
18
|
+
// --- Tiptap UI ---
|
|
19
|
+
import { HeadingButton } from '../heading-button'
|
|
20
|
+
import type { UseHeadingDropdownMenuConfig } from '.'
|
|
21
|
+
import { useHeadingDropdownMenu } from '.'
|
|
22
|
+
|
|
23
|
+
export interface HeadingDropdownMenuProps
|
|
24
|
+
extends Omit<ButtonProps, 'type'>,
|
|
25
|
+
UseHeadingDropdownMenuConfig {
|
|
26
|
+
/**
|
|
27
|
+
* Whether to render the dropdown menu in a portal
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
portal?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Callback for when the dropdown opens or closes
|
|
33
|
+
*/
|
|
34
|
+
onOpenChange?: (isOpen: boolean) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Dropdown menu component for selecting heading levels in a Tiptap editor.
|
|
39
|
+
*
|
|
40
|
+
* For custom dropdown implementations, use the `useHeadingDropdownMenu` hook instead.
|
|
41
|
+
*/
|
|
42
|
+
export const HeadingDropdownMenu = forwardRef<HTMLButtonElement, HeadingDropdownMenuProps>(
|
|
43
|
+
(
|
|
44
|
+
{
|
|
45
|
+
editor: providedEditor,
|
|
46
|
+
levels = [2, 3, 4, 5, 6],
|
|
47
|
+
hideWhenUnavailable = false,
|
|
48
|
+
portal = false,
|
|
49
|
+
onOpenChange,
|
|
50
|
+
children,
|
|
51
|
+
...buttonProps
|
|
52
|
+
},
|
|
53
|
+
ref
|
|
54
|
+
) => {
|
|
55
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
56
|
+
const [isOpen, setIsOpen] = useState<boolean>(false)
|
|
57
|
+
const { isVisible, isActive, canToggle, Icon } = useHeadingDropdownMenu({
|
|
58
|
+
editor,
|
|
59
|
+
levels,
|
|
60
|
+
hideWhenUnavailable
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const handleOpenChange = useCallback(
|
|
64
|
+
(open: boolean) => {
|
|
65
|
+
if (!editor || !canToggle) return
|
|
66
|
+
setIsOpen(open)
|
|
67
|
+
onOpenChange?.(open)
|
|
68
|
+
},
|
|
69
|
+
[canToggle, editor, onOpenChange]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if (!isVisible) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
|
|
78
|
+
<DropdownMenuTrigger asChild>
|
|
79
|
+
<Button
|
|
80
|
+
type="button"
|
|
81
|
+
variant="ghost"
|
|
82
|
+
data-active-state={isActive ? 'on' : 'off'}
|
|
83
|
+
role="button"
|
|
84
|
+
tabIndex={-1}
|
|
85
|
+
disabled={!canToggle}
|
|
86
|
+
data-disabled={!canToggle}
|
|
87
|
+
aria-label="Format text as heading"
|
|
88
|
+
aria-pressed={isActive}
|
|
89
|
+
tooltip="Heading"
|
|
90
|
+
{...buttonProps}
|
|
91
|
+
ref={ref}
|
|
92
|
+
onPointerDown={(e) => e.preventDefault()}
|
|
93
|
+
onClick={() => handleOpenChange(!isOpen)}
|
|
94
|
+
>
|
|
95
|
+
{children ? (
|
|
96
|
+
children
|
|
97
|
+
) : (
|
|
98
|
+
<>
|
|
99
|
+
<Icon className="tiptap-button-icon" />
|
|
100
|
+
<ChevronDownIcon className="tiptap-button-dropdown-small" />
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</Button>
|
|
104
|
+
</DropdownMenuTrigger>
|
|
105
|
+
|
|
106
|
+
<DropdownMenuContent align="start" portal={portal}>
|
|
107
|
+
<Card>
|
|
108
|
+
<CardBody>
|
|
109
|
+
<ButtonGroup>
|
|
110
|
+
{levels.map((level) => (
|
|
111
|
+
<DropdownMenuItem key={`heading-${level}`} asChild>
|
|
112
|
+
<HeadingButton
|
|
113
|
+
editor={editor}
|
|
114
|
+
level={level}
|
|
115
|
+
text={`Heading ${level}`}
|
|
116
|
+
showTooltip={false}
|
|
117
|
+
/>
|
|
118
|
+
</DropdownMenuItem>
|
|
119
|
+
))}
|
|
120
|
+
</ButtonGroup>
|
|
121
|
+
</CardBody>
|
|
122
|
+
</Card>
|
|
123
|
+
</DropdownMenuContent>
|
|
124
|
+
</DropdownMenu>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
HeadingDropdownMenu.displayName = 'HeadingDropdownMenu'
|
|
130
|
+
|
|
131
|
+
export default HeadingDropdownMenu
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Editor } from '@tiptap/react'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
// --- Hooks ---
|
|
7
|
+
import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
|
|
8
|
+
|
|
9
|
+
// --- Icons ---
|
|
10
|
+
import { HeadingIcon } from '../../tiptap-icons/heading-icon'
|
|
11
|
+
|
|
12
|
+
// --- Tiptap UI ---
|
|
13
|
+
import {
|
|
14
|
+
canToggle,
|
|
15
|
+
headingIcons,
|
|
16
|
+
isHeadingActive,
|
|
17
|
+
type Level,
|
|
18
|
+
shouldShowButton
|
|
19
|
+
} from '../heading-button'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configuration for the heading dropdown menu functionality
|
|
23
|
+
*/
|
|
24
|
+
export interface UseHeadingDropdownMenuConfig {
|
|
25
|
+
/**
|
|
26
|
+
* The Tiptap editor instance.
|
|
27
|
+
*/
|
|
28
|
+
editor?: Editor | null
|
|
29
|
+
/**
|
|
30
|
+
* Available heading levels to show in the dropdown
|
|
31
|
+
* @default [1, 2, 3, 4, 5, 6]
|
|
32
|
+
*/
|
|
33
|
+
levels?: Level[]
|
|
34
|
+
/**
|
|
35
|
+
* Whether the dropdown should hide when headings are not available.
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
hideWhenUnavailable?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets the currently active heading level from the available levels
|
|
43
|
+
*/
|
|
44
|
+
export function getActiveHeadingLevel(
|
|
45
|
+
editor: Editor | null,
|
|
46
|
+
levels: Level[] = [1, 2, 3, 4, 5, 6]
|
|
47
|
+
): Level | undefined {
|
|
48
|
+
if (!editor || !editor.isEditable) return undefined
|
|
49
|
+
return levels.find((level) => isHeadingActive(editor, level))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Custom hook that provides heading dropdown menu functionality for Tiptap editor
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* // Simple usage
|
|
58
|
+
* function MyHeadingDropdown() {
|
|
59
|
+
* const {
|
|
60
|
+
* isVisible,
|
|
61
|
+
* activeLevel,
|
|
62
|
+
* isAnyHeadingActive,
|
|
63
|
+
* canToggle,
|
|
64
|
+
* levels,
|
|
65
|
+
* } = useHeadingDropdownMenu()
|
|
66
|
+
*
|
|
67
|
+
* if (!isVisible) return null
|
|
68
|
+
*
|
|
69
|
+
* return (
|
|
70
|
+
* <DropdownMenu>
|
|
71
|
+
* // dropdown content
|
|
72
|
+
* </DropdownMenu>
|
|
73
|
+
* )
|
|
74
|
+
* }
|
|
75
|
+
*
|
|
76
|
+
* // Advanced usage with configuration
|
|
77
|
+
* function MyAdvancedHeadingDropdown() {
|
|
78
|
+
* const {
|
|
79
|
+
* isVisible,
|
|
80
|
+
* activeLevel,
|
|
81
|
+
* } = useHeadingDropdownMenu({
|
|
82
|
+
* editor: myEditor,
|
|
83
|
+
* levels: [1, 2, 3],
|
|
84
|
+
* hideWhenUnavailable: true,
|
|
85
|
+
* })
|
|
86
|
+
*
|
|
87
|
+
* // component implementation
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function useHeadingDropdownMenu(config?: UseHeadingDropdownMenuConfig) {
|
|
92
|
+
const {
|
|
93
|
+
editor: providedEditor,
|
|
94
|
+
levels = [1, 2, 3, 4, 5, 6],
|
|
95
|
+
hideWhenUnavailable = false
|
|
96
|
+
} = config || {}
|
|
97
|
+
|
|
98
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
99
|
+
const [isVisible, setIsVisible] = useState(true)
|
|
100
|
+
|
|
101
|
+
const activeLevel = getActiveHeadingLevel(editor, levels)
|
|
102
|
+
const isActive = isHeadingActive(editor)
|
|
103
|
+
const canToggleState = canToggle(editor)
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!editor) return
|
|
107
|
+
|
|
108
|
+
const handleSelectionUpdate = () => {
|
|
109
|
+
setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, level: levels }))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
handleSelectionUpdate()
|
|
113
|
+
|
|
114
|
+
editor.on('selectionUpdate', handleSelectionUpdate)
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
editor.off('selectionUpdate', handleSelectionUpdate)
|
|
118
|
+
}
|
|
119
|
+
}, [editor, hideWhenUnavailable, levels])
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
isVisible,
|
|
123
|
+
activeLevel,
|
|
124
|
+
isActive,
|
|
125
|
+
canToggle: canToggleState,
|
|
126
|
+
levels,
|
|
127
|
+
label: 'Heading',
|
|
128
|
+
Icon: activeLevel ? headingIcons[activeLevel] : HeadingIcon
|
|
129
|
+
}
|
|
130
|
+
}
|