| 1 | "use client"; |
| 2 | |
| 3 | import { insertCallout } from "@platejs/callout"; |
| 4 | import { insertCodeBlock } from "@platejs/code-block"; |
| 5 | import { insertDate } from "@platejs/date"; |
| 6 | import { insertColumnGroup, toggleColumnGroup } from "@platejs/layout"; |
| 7 | import { triggerFloatingLink } from "@platejs/link/react"; |
| 8 | import { isOrderedList } from "@platejs/list"; |
| 9 | import { insertEquation, insertInlineEquation } from "@platejs/math"; |
| 10 | import { |
| 11 | insertAudioPlaceholder, |
| 12 | insertFilePlaceholder, |
| 13 | insertMedia, |
| 14 | insertVideoPlaceholder, |
| 15 | } from "@platejs/media"; |
| 16 | import { SuggestionPlugin } from "@platejs/suggestion/react"; |
| 17 | import { TablePlugin } from "@platejs/table/react"; |
| 18 | import { insertToc } from "@platejs/toc"; |
| 19 | import { |
| 20 | KEYS, |
| 21 | PathApi, |
| 22 | type NodeEntry, |
| 23 | type Path, |
| 24 | type TElement, |
| 25 | } from "platejs"; |
| 26 | import { type PlateEditor } from "platejs/react"; |
| 27 | |
| 28 | const ACTION_THREE_COLUMNS = "action_three_columns"; |
| 29 | const LIST_STYLE_TYPE_KEY = "listStyleType"; |
| 30 | const LIST_START_KEY = "listStart"; |
| 31 | const LIST_RESTART_KEY = "listRestart"; |
| 32 | const LIST_RESTART_POLITE_KEY = "listRestartPolite"; |
| 33 | |
| 34 | const insertList = (editor: PlateEditor, type: string) => { |
| 35 | editor.tf.insertNodes( |
| 36 | editor.api.create.block({ |
| 37 | indent: 1, |
| 38 | listStyleType: type, |
| 39 | }), |
| 40 | { select: true }, |
| 41 | ); |
| 42 | }; |
| 43 | |
| 44 | const insertBlockMap: Record< |
| 45 | string, |
| 46 | (editor: PlateEditor, type: string, props?: Partial<TElement>) => void |
| 47 | > = { |
| 48 | [KEYS.listTodo]: insertList, |
| 49 | [KEYS.ol]: insertList, |
| 50 | [KEYS.ul]: insertList, |
| 51 | [ACTION_THREE_COLUMNS]: (editor) => |
| 52 | insertColumnGroup(editor, { columns: 3, select: true }), |
| 53 | [KEYS.audio]: (editor) => insertAudioPlaceholder(editor, { select: true }), |
| 54 | [KEYS.callout]: (editor, _type, props) => |
| 55 | insertCallout(editor, { |
| 56 | icon: getStringProp(props, "icon"), |
| 57 | select: true, |
| 58 | variant: getStringProp(props, "variant"), |
| 59 | }), |
| 60 | [KEYS.codeBlock]: (editor) => insertCodeBlock(editor, { select: true }), |
| 61 | [KEYS.equation]: (editor) => insertEquation(editor, { select: true }), |
| 62 | [KEYS.file]: (editor) => insertFilePlaceholder(editor, { select: true }), |
| 63 | [KEYS.img]: (editor) => |
| 64 | insertMedia(editor, { |
| 65 | select: true, |
| 66 | type: KEYS.img, |
| 67 | }), |
| 68 | [KEYS.mediaEmbed]: (editor) => |
| 69 | insertMedia(editor, { |
| 70 | select: true, |
| 71 | type: KEYS.mediaEmbed, |
| 72 | }), |
| 73 | [KEYS.table]: (editor) => |
| 74 | editor.getTransforms(TablePlugin).insert.table({}, { select: true }), |
| 75 | [KEYS.toc]: (editor) => insertToc(editor, { select: true }), |
| 76 | [KEYS.video]: (editor) => insertVideoPlaceholder(editor, { select: true }), |
| 77 | }; |
| 78 | |
| 79 | const insertInlineMap: Record< |
| 80 | string, |
| 81 | (editor: PlateEditor, type: string) => void |
| 82 | > = { |
| 83 | [KEYS.date]: (editor) => insertDate(editor, { select: true }), |
| 84 | [KEYS.inlineEquation]: (editor) => |
| 85 | insertInlineEquation(editor, "", { select: true }), |
| 86 | [KEYS.link]: (editor) => triggerFloatingLink(editor, { focused: true }), |
| 87 | }; |
| 88 | |
| 89 | export const insertBlock = ( |
| 90 | editor: PlateEditor, |
| 91 | type: string, |
| 92 | { props }: { props?: Partial<TElement> } = {}, |
| 93 | ) => { |
| 94 | editor.tf.withoutNormalizing(() => { |
| 95 | const block = editor.api.block(); |
| 96 | |
| 97 | if (!block) return; |
| 98 | if (type in insertBlockMap) { |
| 99 | insertBlockMap[type]!(editor, type, props); |
| 100 | } else { |
| 101 | editor.tf.insertNodes(editor.api.create.block({ ...props, type }), { |
| 102 | at: PathApi.next(block[1]), |
| 103 | select: true, |
| 104 | }); |
| 105 | } |
| 106 | if (getBlockType(block[0]) !== type) { |
| 107 | editor.getApi(SuggestionPlugin).suggestion.withoutSuggestions(() => { |
| 108 | editor.tf.removeNodes({ previousEmptyBlock: true }); |
| 109 | }); |
| 110 | } |
| 111 | }); |
| 112 | }; |
| 113 | |
| 114 | function getStringProp(props: Partial<TElement> | undefined, key: string) { |
| 115 | const value = props?.[key]; |
| 116 | |
| 117 | return typeof value === "string" ? value : undefined; |
| 118 | } |
| 119 | |
| 120 | export const insertInlineElement = (editor: PlateEditor, type: string) => { |
| 121 | if (insertInlineMap[type]) { |
| 122 | insertInlineMap[type](editor, type); |
| 123 | } |
| 124 | }; |
| 125 | |
| 126 | const setList = ( |
| 127 | editor: PlateEditor, |
| 128 | type: string, |
| 129 | entry: NodeEntry<TElement>, |
| 130 | ) => { |
| 131 | editor.tf.setNodes( |
| 132 | editor.api.create.block({ |
| 133 | indent: 1, |
| 134 | listStyleType: type, |
| 135 | }), |
| 136 | { |
| 137 | at: entry[1], |
| 138 | }, |
| 139 | ); |
| 140 | }; |
| 141 | |
| 142 | const setBlockMap: Record< |
| 143 | string, |
| 144 | (editor: PlateEditor, type: string, entry: NodeEntry<TElement>) => void |
| 145 | > = { |
| 146 | [KEYS.listTodo]: setList, |
| 147 | [KEYS.ol]: setList, |
| 148 | [KEYS.ul]: setList, |
| 149 | [ACTION_THREE_COLUMNS]: (editor) => toggleColumnGroup(editor, { columns: 3 }), |
| 150 | }; |
| 151 | |
| 152 | type SetBlockTypeOptions = { |
| 153 | at?: Path; |
| 154 | props?: Partial<TElement>; |
| 155 | }; |
| 156 | |
| 157 | export const setBlockType = ( |
| 158 | editor: PlateEditor, |
| 159 | type: string, |
| 160 | { at, props }: SetBlockTypeOptions = {}, |
| 161 | ) => { |
| 162 | editor.tf.withoutNormalizing(() => { |
| 163 | const setEntry = (entry: NodeEntry<TElement>) => { |
| 164 | const [node, path] = entry; |
| 165 | |
| 166 | if (node[KEYS.listType] || node[LIST_STYLE_TYPE_KEY]) { |
| 167 | editor.tf.unsetNodes( |
| 168 | [ |
| 169 | KEYS.listType, |
| 170 | "indent", |
| 171 | LIST_STYLE_TYPE_KEY, |
| 172 | LIST_START_KEY, |
| 173 | LIST_RESTART_KEY, |
| 174 | LIST_RESTART_POLITE_KEY, |
| 175 | ], |
| 176 | { at: path }, |
| 177 | ); |
| 178 | } |
| 179 | if (type in setBlockMap) { |
| 180 | return setBlockMap[type]!(editor, type, entry); |
| 181 | } |
| 182 | if (node.type !== type) { |
| 183 | editor.tf.setNodes({ ...props, type }, { at: path }); |
| 184 | } else if (props) { |
| 185 | editor.tf.setNodes(props, { at: path }); |
| 186 | } |
| 187 | }; |
| 188 | |
| 189 | if (at) { |
| 190 | const entry = editor.api.node(at) as NodeEntry<TElement> | undefined; |
| 191 | |
| 192 | if (entry) { |
| 193 | setEntry(entry); |
| 194 | |
| 195 | return; |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | const entries = editor.api.blocks({ mode: "lowest" }); |
| 200 | |
| 201 | entries.forEach((entry) => setEntry(entry)); |
| 202 | }); |
| 203 | }; |
| 204 | |
| 205 | export const getBlockType = (block: TElement) => { |
| 206 | if (typeof block[LIST_STYLE_TYPE_KEY] === "string") { |
| 207 | if (block[LIST_STYLE_TYPE_KEY] === KEYS.listTodo) { |
| 208 | return KEYS.listTodo; |
| 209 | } |
| 210 | |
| 211 | return isOrderedList(block) ? KEYS.ol : KEYS.ul; |
| 212 | } |
| 213 | |
| 214 | if (block[KEYS.listType]) { |
| 215 | if (block[KEYS.listType] === KEYS.ol) { |
| 216 | return KEYS.ol; |
| 217 | } else if (block[KEYS.listType] === KEYS.listTodo) { |
| 218 | return KEYS.listTodo; |
| 219 | } else { |
| 220 | return KEYS.ul; |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | return block.type; |
| 225 | }; |
| 226 |