| 1 | "use client"; |
| 2 | |
| 3 | import { toUnitLess } from "@platejs/basic-styles"; |
| 4 | import { FontSizePlugin } from "@platejs/basic-styles/react"; |
| 5 | import { Minus, Plus } from "lucide-react"; |
| 6 | import { KEYS, type NodeEntry, type TElement } from "platejs"; |
| 7 | import { useEditorPlugin, useEditorSelector } from "platejs/react"; |
| 8 | import * as React from "react"; |
| 9 | |
| 10 | import { |
| 11 | Popover, |
| 12 | PopoverContent, |
| 13 | PopoverTrigger, |
| 14 | } from "@/components/plate/ui/popover"; |
| 15 | import { cn } from "@/lib/utils"; |
| 16 | import { ToolbarButton } from "./toolbar"; |
| 17 | |
| 18 | const DEFAULT_FONT_SIZE = "16"; |
| 19 | |
| 20 | const FONT_SIZE_MAP = { |
| 21 | h1: "36", |
| 22 | h2: "24", |
| 23 | h3: "20", |
| 24 | } as const; |
| 25 | |
| 26 | const FONT_SIZES = [ |
| 27 | "8", |
| 28 | "9", |
| 29 | "10", |
| 30 | "12", |
| 31 | "14", |
| 32 | "16", |
| 33 | "18", |
| 34 | "24", |
| 35 | "30", |
| 36 | "36", |
| 37 | "48", |
| 38 | "60", |
| 39 | "72", |
| 40 | "96", |
| 41 | ] as const; |
| 42 | |
| 43 | export function FontSizeToolbarButton() { |
| 44 | const [inputValue, setInputValue] = React.useState(DEFAULT_FONT_SIZE); |
| 45 | const [isFocused, setIsFocused] = React.useState(false); |
| 46 | const { editor, tf } = useEditorPlugin(FontSizePlugin); |
| 47 | |
| 48 | const cursorFontSize = useEditorSelector((editor) => { |
| 49 | const fontSize = editor.api.marks()?.[KEYS.fontSize]; |
| 50 | |
| 51 | if (fontSize) { |
| 52 | return toUnitLess(fontSize as string); |
| 53 | } |
| 54 | |
| 55 | const blockEntry = editor.api.block() as NodeEntry<TElement> | undefined; |
| 56 | const block = blockEntry?.[0]; |
| 57 | |
| 58 | if (!block?.type) return DEFAULT_FONT_SIZE; |
| 59 | |
| 60 | return block.type in FONT_SIZE_MAP |
| 61 | ? FONT_SIZE_MAP[block.type as keyof typeof FONT_SIZE_MAP] |
| 62 | : DEFAULT_FONT_SIZE; |
| 63 | }, []); |
| 64 | |
| 65 | const handleInputChange = () => { |
| 66 | const newSize = toUnitLess(inputValue); |
| 67 | |
| 68 | if ( |
| 69 | Number.parseInt(newSize, 10) < 1 || |
| 70 | Number.parseInt(newSize, 10) > 100 |
| 71 | ) { |
| 72 | editor.tf.focus(); |
| 73 | |
| 74 | return; |
| 75 | } |
| 76 | if (newSize !== toUnitLess(cursorFontSize)) { |
| 77 | tf.fontSize.addMark(`${newSize}px`); |
| 78 | } |
| 79 | |
| 80 | editor.tf.focus(); |
| 81 | }; |
| 82 | |
| 83 | const handleFontSizeChange = (delta: number) => { |
| 84 | const newSize = Number(displayValue) + delta; |
| 85 | tf.fontSize.addMark(`${newSize}px`); |
| 86 | editor.tf.focus(); |
| 87 | }; |
| 88 | |
| 89 | const displayValue = isFocused ? inputValue : cursorFontSize; |
| 90 | |
| 91 | return ( |
| 92 | <div className="flex h-7 items-center gap-1 rounded-md bg-muted/60 p-0"> |
| 93 | <ToolbarButton onClick={() => handleFontSizeChange(-1)}> |
| 94 | <Minus /> |
| 95 | </ToolbarButton> |
| 96 | |
| 97 | <Popover open={isFocused} modal={false}> |
| 98 | <PopoverTrigger asChild> |
| 99 | <input |
| 100 | aria-label="font size toolbar button control" |
| 101 | className={cn( |
| 102 | "h-full w-10 shrink-0 bg-transparent px-1 text-center text-sm hover:bg-muted", |
| 103 | )} |
| 104 | value={displayValue} |
| 105 | onBlur={() => { |
| 106 | setIsFocused(false); |
| 107 | handleInputChange(); |
| 108 | }} |
| 109 | onChange={(e) => setInputValue(e.target.value)} |
| 110 | onFocus={() => { |
| 111 | setIsFocused(true); |
| 112 | setInputValue(toUnitLess(cursorFontSize)); |
| 113 | }} |
| 114 | onKeyDown={(e) => { |
| 115 | if (e.key === "Enter") { |
| 116 | e.preventDefault(); |
| 117 | handleInputChange(); |
| 118 | } |
| 119 | }} |
| 120 | data-plate-focus="true" |
| 121 | type="text" |
| 122 | /> |
| 123 | </PopoverTrigger> |
| 124 | <PopoverContent |
| 125 | className="w-10 px-px py-1" |
| 126 | onOpenAutoFocus={(e) => e.preventDefault()} |
| 127 | > |
| 128 | {FONT_SIZES.map((size) => ( |
| 129 | <button |
| 130 | key={size} |
| 131 | className={cn( |
| 132 | "flex h-8 w-full items-center justify-center text-sm hover:bg-accent data-[highlighted=true]:bg-accent", |
| 133 | )} |
| 134 | onClick={() => { |
| 135 | tf.fontSize.addMark(`${size}px`); |
| 136 | setIsFocused(false); |
| 137 | }} |
| 138 | data-highlighted={size === displayValue} |
| 139 | type="button" |
| 140 | > |
| 141 | {size} |
| 142 | </button> |
| 143 | ))} |
| 144 | </PopoverContent> |
| 145 | </Popover> |
| 146 | |
| 147 | <ToolbarButton onClick={() => handleFontSizeChange(1)}> |
| 148 | <Plus /> |
| 149 | </ToolbarButton> |
| 150 | </div> |
| 151 | ); |
| 152 | } |
| 153 |