| ... | ... | @@ -28,7 +28,10 @@ import { |
| 28 | 28 | KEY_ENTER_COMMAND, |
| 29 | 29 | KEY_TAB_COMMAND, |
| 30 | 30 | COMMAND_PRIORITY_HIGH, |
| 31 | COMMAND_PRIORITY_LOW, |
| 31 | 32 | KEY_BACKSPACE_COMMAND, |
| 33 | BLUR_COMMAND, |
| 34 | FOCUS_COMMAND, |
| 32 | 35 | $getRoot, |
| 33 | 36 | HISTORY_MERGE_TAG, |
| 34 | 37 | DecoratorNode, |
| ... | ... | @@ -185,7 +188,7 @@ class ComposerMentionNode extends DecoratorNode<React.ReactElement> { |
| 185 | 188 | |
| 186 | 189 | override createDOM(): HTMLElement { |
| 187 | 190 | const dom = document.createElement("span"); |
| 188 | | dom.className = "inline-flex align-middle leading-none"; |
| 191 | dom.className = "composer-inline-chip relative inline-flex align-middle leading-none"; |
| 189 | 192 | return dom; |
| 190 | 193 | } |
| 191 | 194 | |
| ... | ... | @@ -323,7 +326,7 @@ class ComposerSkillNode extends DecoratorNode<React.ReactElement> { |
| 323 | 326 | |
| 324 | 327 | override createDOM(): HTMLElement { |
| 325 | 328 | const dom = document.createElement("span"); |
| 326 | | dom.className = "inline-flex align-middle leading-none"; |
| 329 | dom.className = "composer-inline-chip relative inline-flex align-middle leading-none"; |
| 327 | 330 | return dom; |
| 328 | 331 | } |
| 329 | 332 | |
| ... | ... | @@ -394,7 +397,7 @@ class ComposerTerminalContextNode extends DecoratorNode<React.ReactElement> { |
| 394 | 397 | |
| 395 | 398 | override createDOM(): HTMLElement { |
| 396 | 399 | const dom = document.createElement("span"); |
| 397 | | dom.className = "inline-flex align-middle leading-none"; |
| 400 | dom.className = "composer-inline-chip relative inline-flex align-middle leading-none"; |
| 398 | 401 | return dom; |
| 399 | 402 | } |
| 400 | 403 | |
| ... | ... | @@ -1167,6 +1170,80 @@ function ComposerInlineTokenBackspacePlugin() { |
| 1167 | 1170 | return null; |
| 1168 | 1171 | } |
| 1169 | 1172 | |
| 1173 | /** |
| 1174 | * Chips render as non-editable decorators, so the browser never paints the |
| 1175 | * native text selection over them; without help, a selection spanning chips |
| 1176 | * is only visible in the slivers between them. Mirror the selection onto the |
| 1177 | * chips with a data attribute the stylesheet turns into a highlight overlay. |
| 1178 | */ |
| 1179 | function ComposerChipSelectionPlugin() { |
| 1180 | const [editor] = useLexicalComposerContext(); |
| 1181 | |
| 1182 | useEffect(() => { |
| 1183 | let selectedKeys = new Set<string>(); |
| 1184 | // Lexical keeps the range selection on blur without emitting an update, |
| 1185 | // so focus is tracked separately; while blurred the native highlight is |
| 1186 | // gone and the mirrored one has to go with it. |
| 1187 | let hasFocus = editor.getRootElement() === document.activeElement; |
| 1188 | |
| 1189 | const applyKeys = (nextKeys: Set<string>) => { |
| 1190 | for (const key of selectedKeys) { |
| 1191 | if (!nextKeys.has(key)) { |
| 1192 | editor.getElementByKey(key)?.removeAttribute("data-composer-chip-selected"); |
| 1193 | } |
| 1194 | } |
| 1195 | for (const key of nextKeys) { |
| 1196 | editor.getElementByKey(key)?.setAttribute("data-composer-chip-selected", "true"); |
| 1197 | } |
| 1198 | selectedKeys = nextKeys; |
| 1199 | }; |
| 1200 | |
| 1201 | const readSelectedKeys = () => { |
| 1202 | const nextKeys = new Set<string>(); |
| 1203 | editor.getEditorState().read(() => { |
| 1204 | const selection = $getSelection(); |
| 1205 | if ($isRangeSelection(selection) && !selection.isCollapsed()) { |
| 1206 | for (const node of selection.getNodes()) { |
| 1207 | if (node instanceof DecoratorNode) { |
| 1208 | nextKeys.add(node.getKey()); |
| 1209 | } |
| 1210 | } |
| 1211 | } |
| 1212 | }); |
| 1213 | return nextKeys; |
| 1214 | }; |
| 1215 | |
| 1216 | const unregisterUpdate = editor.registerUpdateListener(() => { |
| 1217 | applyKeys(hasFocus ? readSelectedKeys() : new Set()); |
| 1218 | }); |
| 1219 | const unregisterFocus = editor.registerCommand( |
| 1220 | FOCUS_COMMAND, |
| 1221 | () => { |
| 1222 | hasFocus = true; |
| 1223 | applyKeys(readSelectedKeys()); |
| 1224 | return false; |
| 1225 | }, |
| 1226 | COMMAND_PRIORITY_LOW, |
| 1227 | ); |
| 1228 | const unregisterBlur = editor.registerCommand( |
| 1229 | BLUR_COMMAND, |
| 1230 | () => { |
| 1231 | hasFocus = false; |
| 1232 | applyKeys(new Set()); |
| 1233 | return false; |
| 1234 | }, |
| 1235 | COMMAND_PRIORITY_LOW, |
| 1236 | ); |
| 1237 | return () => { |
| 1238 | unregisterUpdate(); |
| 1239 | unregisterFocus(); |
| 1240 | unregisterBlur(); |
| 1241 | }; |
| 1242 | }, [editor]); |
| 1243 | |
| 1244 | return null; |
| 1245 | } |
| 1246 | |
| 1170 | 1247 | function ComposerInlineTokenPastePlugin() { |
| 1171 | 1248 | const [editor] = useLexicalComposerContext(); |
| 1172 | 1249 | |
| ... | ... | @@ -1701,6 +1778,7 @@ function ComposerPromptEditorInner({ |
| 1701 | 1778 | <ComposerInlineTokenSelectionNormalizePlugin /> |
| 1702 | 1779 | <ComposerInlineTokenBackspacePlugin /> |
| 1703 | 1780 | <ComposerInlineTokenPastePlugin /> |
| 1781 | <ComposerChipSelectionPlugin /> |
| 1704 | 1782 | <HistoryPlugin /> |
| 1705 | 1783 | </div> |
| 1706 | 1784 | </ComposerTerminalContextActionsContext> |