|
1 | | -import SelectBox from '@components/ui/SelectBox' |
2 | | -import React, { useCallback } from 'react' |
3 | | - |
4 | | -const SelectHeadingBox = ({ editor }: any) => { |
5 | | - const options = [ |
6 | | - { value: 'p', label: 'Normal Text', className: 'text-[14px]' }, |
7 | | - { value: 1, label: 'Heading 1', className: 'text-[20px]' }, |
8 | | - { value: 2, label: 'Heading 2', className: 'text-[18px]' }, |
9 | | - { value: 3, label: 'Heading 3', className: 'text-[17px]' }, |
10 | | - { value: 4, label: 'Heading 4', className: 'text-[16px]' }, |
11 | | - { value: 5, label: 'Heading 5', className: 'text-[15px]' } |
12 | | - ] |
13 | | - |
14 | | - const restOptions = [ |
15 | | - { value: 6, label: 'Heading 6', className: 'text-[14px]' }, |
16 | | - { value: 7, label: 'Heading 7', className: 'text-[13px]' }, |
17 | | - { value: 8, label: 'Heading 8', className: 'text-[13px]' }, |
18 | | - { value: 9, label: 'Heading 9', className: 'text-[13px]' }, |
19 | | - { value: 10, label: 'Heading 10', className: 'text-[13px]' } |
20 | | - ] |
21 | | - |
22 | | - const subOptions = { |
23 | | - summary: 'More', |
24 | | - options: restOptions |
25 | | - } |
26 | | - |
27 | | - const onHeadingChange = useCallback( |
28 | | - (value: string) => { |
| 1 | +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' |
| 2 | +import { IoMdArrowDropdown } from 'react-icons/io' |
| 3 | +import { MdCheck } from 'react-icons/md' |
| 4 | + |
| 5 | +const MAIN_OPTIONS = [ |
| 6 | + { value: 'p', label: 'Normal Text' }, |
| 7 | + { value: 1, label: 'Heading 1' }, |
| 8 | + { value: 2, label: 'Heading 2' }, |
| 9 | + { value: 3, label: 'Heading 3' }, |
| 10 | + { value: 4, label: 'Heading 4' }, |
| 11 | + { value: 5, label: 'Heading 5' } |
| 12 | +] |
| 13 | + |
| 14 | +const MORE_OPTIONS = [ |
| 15 | + { value: 6, label: 'Heading 6' }, |
| 16 | + { value: 7, label: 'Heading 7' }, |
| 17 | + { value: 8, label: 'Heading 8' }, |
| 18 | + { value: 9, label: 'Heading 9' }, |
| 19 | + { value: 10, label: 'Heading 10' } |
| 20 | +] |
| 21 | + |
| 22 | +const ALL_OPTIONS = [...MAIN_OPTIONS, ...MORE_OPTIONS] |
| 23 | + |
| 24 | +// Font size classes for visual hierarchy |
| 25 | +const FONT_STYLES: Record<string | number, string> = { |
| 26 | + p: 'text-sm', |
| 27 | + 1: 'text-lg font-semibold', |
| 28 | + 2: 'text-base font-semibold', |
| 29 | + 3: 'text-base font-medium', |
| 30 | + 4: 'text-sm font-medium', |
| 31 | + 5: 'text-sm', |
| 32 | + 6: 'text-xs', |
| 33 | + 7: 'text-xs', |
| 34 | + 8: 'text-xs', |
| 35 | + 9: 'text-xs', |
| 36 | + 10: 'text-xs' |
| 37 | +} |
| 38 | + |
| 39 | +interface SelectHeadingBoxProps { |
| 40 | + editor: any |
| 41 | +} |
| 42 | + |
| 43 | +const SelectHeadingBox = ({ editor }: SelectHeadingBoxProps) => { |
| 44 | + const detailsRef = useRef<HTMLDetailsElement>(null) |
| 45 | + const [showMore, setShowMore] = useState(false) |
| 46 | + |
| 47 | + // Close dropdown when clicking outside |
| 48 | + useEffect(() => { |
| 49 | + const handleClickOutside = (e: MouseEvent) => { |
| 50 | + if (detailsRef.current && !detailsRef.current.contains(e.target as Node)) { |
| 51 | + detailsRef.current.removeAttribute('open') |
| 52 | + setShowMore(false) |
| 53 | + } |
| 54 | + } |
| 55 | + document.addEventListener('click', handleClickOutside) |
| 56 | + return () => document.removeEventListener('click', handleClickOutside) |
| 57 | + }, []) |
| 58 | + |
| 59 | + const currentHeading = useMemo(() => { |
| 60 | + const active = ALL_OPTIONS.find((opt) => |
| 61 | + opt.value === 'p' |
| 62 | + ? !editor.isActive('contentHeading') |
| 63 | + : editor.isActive('contentHeading', { level: opt.value }) |
| 64 | + ) |
| 65 | + return active || MAIN_OPTIONS[0] |
| 66 | + // eslint-disable-next-line react-hooks/exhaustive-deps |
| 67 | + }, [editor.state.selection]) |
| 68 | + |
| 69 | + const handleSelect = useCallback( |
| 70 | + (value: string | number) => { |
29 | 71 | if (value === 'p') { |
30 | 72 | editor.chain().focus().normalText().run() |
31 | 73 | } else { |
32 | 74 | editor.chain().focus().wrapBlock({ level: +value }).run() |
33 | 75 | } |
| 76 | + detailsRef.current?.removeAttribute('open') |
| 77 | + setShowMore(false) |
34 | 78 | }, |
35 | 79 | [editor] |
36 | 80 | ) |
37 | 81 |
|
38 | | - function getCurrentHeading(editor: any): any { |
39 | | - const newOptions = [...options, ...restOptions] |
40 | | - const selectedHeadingOption = newOptions.find((option) => { |
41 | | - return editor.isActive('contentHeading', { level: option.value }) |
42 | | - }) |
43 | | - |
44 | | - return selectedHeadingOption || newOptions.at(0) |
45 | | - } |
| 82 | + const isActive = (value: string | number) => currentHeading.value === value |
46 | 83 |
|
47 | 84 | return ( |
48 | 85 | <div className="tooltip tooltip-bottom" data-tip="Styles (⌘+⌥+[0-9])"> |
49 | | - <SelectBox |
50 | | - options={options} |
51 | | - subOptions={subOptions} |
52 | | - value={getCurrentHeading(editor)} |
53 | | - onChange={onHeadingChange} |
54 | | - /> |
| 86 | + <details ref={detailsRef} className="dropdown"> |
| 87 | + <summary className="btn btn-ghost btn-sm h-8 min-h-0 gap-1 px-2 font-normal"> |
| 88 | + <span className="min-w-20 text-left text-sm">{currentHeading.label}</span> |
| 89 | + <IoMdArrowDropdown className="opacity-60" /> |
| 90 | + </summary> |
| 91 | + |
| 92 | + <ul className="dropdown-content menu bg-base-100 border-base-300 z-50 mt-1 w-48 rounded-lg border p-1 shadow-xl"> |
| 93 | + {MAIN_OPTIONS.map((opt) => ( |
| 94 | + <li key={opt.value}> |
| 95 | + <button |
| 96 | + type="button" |
| 97 | + onClick={() => handleSelect(opt.value)} |
| 98 | + className={`flex items-center justify-between rounded-md px-3 py-2 ${FONT_STYLES[opt.value]} ${ |
| 99 | + isActive(opt.value) ? 'bg-primary/10 text-primary' : 'hover:bg-base-200' |
| 100 | + }`}> |
| 101 | + <span>{opt.label}</span> |
| 102 | + {isActive(opt.value) && <MdCheck className="text-primary" size={16} />} |
| 103 | + </button> |
| 104 | + </li> |
| 105 | + ))} |
| 106 | + |
| 107 | + {/* Divider */} |
| 108 | + <li className="bg-base-200 my-1 h-px" role="separator" /> |
| 109 | + |
| 110 | + {/* More toggle */} |
| 111 | + <li> |
| 112 | + <button |
| 113 | + type="button" |
| 114 | + onClick={(e) => { |
| 115 | + e.stopPropagation() |
| 116 | + setShowMore(!showMore) |
| 117 | + }} |
| 118 | + className="text-base-content/60 hover:bg-base-200 flex items-center justify-between rounded-md px-3 py-2 text-xs"> |
| 119 | + <span>More headings</span> |
| 120 | + <IoMdArrowDropdown |
| 121 | + className={`transition-transform ${showMore ? 'rotate-180' : ''}`} |
| 122 | + size={14} |
| 123 | + /> |
| 124 | + </button> |
| 125 | + </li> |
| 126 | + |
| 127 | + {/* Hidden headings */} |
| 128 | + {showMore && |
| 129 | + MORE_OPTIONS.map((opt) => ( |
| 130 | + <li key={opt.value}> |
| 131 | + <button |
| 132 | + type="button" |
| 133 | + onClick={() => handleSelect(opt.value)} |
| 134 | + className={`flex items-center justify-between rounded-md px-3 py-2 pl-5 ${FONT_STYLES[opt.value]} ${ |
| 135 | + isActive(opt.value) ? 'bg-primary/10 text-primary' : 'hover:bg-base-200' |
| 136 | + }`}> |
| 137 | + <span>{opt.label}</span> |
| 138 | + {isActive(opt.value) && <MdCheck className="text-primary" size={16} />} |
| 139 | + </button> |
| 140 | + </li> |
| 141 | + ))} |
| 142 | + </ul> |
| 143 | + </details> |
55 | 144 | </div> |
56 | 145 | ) |
57 | 146 | } |
|
0 commit comments