| @@ -28,7 +28,10 @@ import { · 27 unmodified lines |
| 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> { · 150 unmodified lines |
| 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> { · 131 unmodified lines |
| 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> { · 64 unmodified lines |
| 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() { · 766 unmodified lines |
| 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({ · 528 unmodified lines |
| 1701 | 1778 | <ComposerInlineTokenSelectionNormalizePlugin /> |
| 1702 | 1779 | <ComposerInlineTokenBackspacePlugin /> |
| 1703 | 1780 | <ComposerInlineTokenPastePlugin /> |
| 1781 | + <ComposerChipSelectionPlugin /> |
| 1704 | 1782 | <HistoryPlugin /> |
| 1705 | 1783 | </div> |
| 1706 | 1784 | </ComposerTerminalContextActionsContext> |