@astryxdesign/core 0.5.2-canary.08db6e9 → 0.5.2-canary.093c131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +84 -6
- package/dist/Chat/chatComposerSelection.d.ts +71 -9
- package/dist/Chat/chatComposerSelection.d.ts.map +1 -1
- package/dist/Chat/chatComposerSelection.js +155 -9
- package/dist/Chat/useChatStreamScroll.d.ts.map +1 -1
- package/dist/Chat/useChatStreamScroll.js +64 -72
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +9 -8
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +1 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +35 -20
- package/dist/Layout/LayoutHeader.d.ts +10 -1
- package/dist/Layout/LayoutHeader.d.ts.map +1 -1
- package/dist/Layout/LayoutHeader.js +8 -2
- package/dist/Markdown/parser.d.ts +26 -0
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +241 -61
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +38 -49
- package/dist/PowerSearch/resolveDateTimeRangePart.d.ts +10 -0
- package/dist/PowerSearch/resolveDateTimeRangePart.d.ts.map +1 -0
- package/dist/PowerSearch/resolveDateTimeRangePart.js +30 -0
- package/dist/PowerSearch/usePowerSearchConfig.d.ts.map +1 -1
- package/dist/PowerSearch/usePowerSearchConfig.js +4 -24
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +8 -7
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +8 -8
- package/dist/TabList/TabList.d.ts +19 -1
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +26 -2
- package/dist/TreeList/TreeList.d.ts +1 -1
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +11 -1
- package/dist/astryx.css +2 -3
- package/dist/hooks/useFocusTrap.d.ts +3 -1
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +31 -1
- package/locales/fr-FR.json +12 -0
- package/package.json +3 -2
- package/scripts/agent-doc-state.mjs +137 -0
- package/scripts/postinstall.mjs +28 -60
- package/src/Center/Center.doc.mjs +7 -0
- package/src/Chat/ChatComposerInput.test.tsx +504 -0
- package/src/Chat/ChatComposerInput.tsx +94 -6
- package/src/Chat/ChatComposerTokenElement.doc.mjs +5 -0
- package/src/Chat/ChatMessage.doc.mjs +17 -0
- package/src/Chat/ChatMessageList.doc.mjs +33 -0
- package/src/Chat/ChatToolCalls.doc.mjs +9 -0
- package/src/Chat/chatComposerSelection.ts +189 -9
- package/src/Chat/useChatStreamScroll.test.tsx +168 -5
- package/src/Chat/useChatStreamScroll.ts +71 -67
- package/src/CheckboxInput/CheckboxInput.test.tsx +53 -0
- package/src/CheckboxInput/CheckboxInput.tsx +9 -21
- package/src/Collapsible/Collapsible.tsx +10 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +13 -0
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +31 -0
- package/src/Field/FieldLabel.test.tsx +15 -0
- package/src/Field/FieldLabel.tsx +69 -41
- package/src/Grid/Grid.doc.mjs +11 -0
- package/src/Grid/GridSpan.doc.mjs +7 -0
- package/src/Indicator/Indicator.doc.mjs +3 -0
- package/src/Layout/Layout.test.tsx +64 -0
- package/src/Layout/LayoutHeader.doc.mjs +14 -0
- package/src/Layout/LayoutHeader.tsx +18 -0
- package/src/Layout/LayoutPanel.doc.mjs +18 -0
- package/src/Markdown/Markdown.test.tsx +86 -1
- package/src/Markdown/incremental.test.ts +242 -0
- package/src/Markdown/parser.perf.test.ts +93 -0
- package/src/Markdown/parser.ts +287 -60
- package/src/MultiSelector/MultiSelector.doc.mjs +11 -0
- package/src/PowerSearch/PowerSearch.doc.mjs +45 -0
- package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +123 -0
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +120 -4
- package/src/PowerSearch/PowerSearchValueEditor.tsx +48 -63
- package/src/PowerSearch/resolveDateTimeRangePart.test.ts +40 -0
- package/src/PowerSearch/resolveDateTimeRangePart.ts +37 -0
- package/src/PowerSearch/usePowerSearchConfig.test.ts +26 -1
- package/src/PowerSearch/usePowerSearchConfig.ts +7 -25
- package/src/RadioList/RadioList.test.tsx +49 -0
- package/src/RadioList/RadioListItem.tsx +9 -20
- package/src/Resizable/useResizable.test.ts +52 -0
- package/src/Selector/Selector.doc.mjs +10 -0
- package/src/SideNav/SideNavCollapseButton.doc.mjs +8 -0
- package/src/SideNav/SideNavHeading.doc.mjs +3 -0
- package/src/SideNav/SideNavItem.doc.mjs +3 -0
- package/src/SideNav/SideNavSection.doc.mjs +10 -0
- package/src/Stack/Stack.doc.mjs +38 -0
- package/src/Switch/Switch.test.tsx +53 -0
- package/src/Switch/Switch.tsx +9 -21
- package/src/TabList/TabList.doc.mjs +10 -1
- package/src/TabList/TabList.tsx +51 -0
- package/src/TabList/TabMenu.doc.mjs +14 -0
- package/src/Toast/Toast.doc.mjs +4 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +14 -0
- package/src/TopNav/TopNavHeading.doc.mjs +3 -0
- package/src/TopNav/TopNavItem.doc.mjs +3 -0
- package/src/TopNav/TopNavMenu.doc.mjs +10 -0
- package/src/TreeList/TreeList.test.tsx +94 -0
- package/src/TreeList/TreeList.tsx +19 -1
- package/src/hooks/useFocusTrap.doc.mjs +2 -2
- package/src/hooks/useFocusTrap.test.tsx +48 -0
- package/src/hooks/useFocusTrap.ts +35 -1
- package/src/postinstall.test.mjs +71 -5
- package/src/theme/extensibleAxes.test.ts +26 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatComposerInput.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatComposerInput.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAML,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"ChatComposerInput.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatComposerInput.tsx"],"names":[],"mappings":"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"}
|
|
@@ -30,7 +30,7 @@ import { colorVars, spacingVars, typeScaleVars, typographyVars } from "../theme/
|
|
|
30
30
|
import { mergeProps, isImeKeyEvent } from "../utils/index.js";
|
|
31
31
|
import { useTriggerMenu } from "./useTriggerMenu.js";
|
|
32
32
|
import { useChatComposerTokens, isCustomToken } from "./useChatComposerTokens.js";
|
|
33
|
-
import { ensureCaretInside, insertTextAtCursor } from "./chatComposerSelection.js";
|
|
33
|
+
import { ensureCaretInside, insertTextAtCursor, isSelectionAtStart, isSelectionAtEnd, placeCaretAtEnd, getSelectionRangeInside, restoreSelectionRange } from "./chatComposerSelection.js";
|
|
34
34
|
import { ChatPastedTextToken } from "./ChatPastedTextToken.js";
|
|
35
35
|
import { useChatPasteAsToken } from "./useChatPasteAsToken.js";
|
|
36
36
|
import { Badge } from "../Badge/index.js";
|
|
@@ -182,11 +182,65 @@ export function ChatComposerInput(props) {
|
|
|
182
182
|
// when a parent attaches a ref — without this, paste-as-token would
|
|
183
183
|
// silently no-op whenever `ChatComposerInput` is rendered without
|
|
184
184
|
// a forwarded ref (e.g. inside `ChatComposer`).
|
|
185
|
+
/**
|
|
186
|
+
* Focus the editable and put the caret after the draft.
|
|
187
|
+
*
|
|
188
|
+
* A bare `focus()` is not enough: Chromium collapses the caret to the
|
|
189
|
+
* start of the content, which is the one position where ArrowUp means
|
|
190
|
+
* "recall history" — so focusing a composer that already holds a draft
|
|
191
|
+
* would arm the next ArrowUp to replace it. Landing after the text is
|
|
192
|
+
* also what a click on the composer's trailing space means.
|
|
193
|
+
*/
|
|
194
|
+
/**
|
|
195
|
+
* Focus the editable, keeping a caret or selection the user already has
|
|
196
|
+
* inside it.
|
|
197
|
+
*
|
|
198
|
+
* A bare `focus()` is not enough on its own: Chromium collapses the caret
|
|
199
|
+
* to the start of the content, which is the one position where ArrowUp
|
|
200
|
+
* means "recall history" — so focusing a composer that already holds a
|
|
201
|
+
* draft would arm the next ArrowUp to replace it. But a consumer calling
|
|
202
|
+
* `focus()` to return the user to where they were must not have their
|
|
203
|
+
* caret moved either, so an existing in-editor selection is captured
|
|
204
|
+
* before focusing and restored after. Only when there is none does the
|
|
205
|
+
* caret land after the draft.
|
|
206
|
+
*/
|
|
207
|
+
const focusEditable = useCallback(() => {
|
|
208
|
+
const editable = editableRef.current;
|
|
209
|
+
if (!editable) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Read before focusing: `focus()` itself creates the offset-0 caret, so
|
|
213
|
+
// asking afterwards cannot tell the user's own caret from the engine's.
|
|
214
|
+
const existing = getSelectionRangeInside(editable);
|
|
215
|
+
editable.focus();
|
|
216
|
+
if (existing) {
|
|
217
|
+
restoreSelectionRange(existing);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
placeCaretAtEnd(editable);
|
|
221
|
+
}, []);
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Focus the editable and put the caret after the draft, whatever the
|
|
225
|
+
* selection was.
|
|
226
|
+
*
|
|
227
|
+
* This is the composer shell's click-to-focus path: clicking the empty
|
|
228
|
+
* space after a draft means "put me after the text", so it overrides a
|
|
229
|
+
* stale caret rather than restoring one.
|
|
230
|
+
*/
|
|
231
|
+
const focusEditableAtEnd = useCallback(() => {
|
|
232
|
+
const editable = editableRef.current;
|
|
233
|
+
if (!editable) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
editable.focus();
|
|
237
|
+
placeCaretAtEnd(editable);
|
|
238
|
+
}, []);
|
|
185
239
|
const handle = {
|
|
186
240
|
insertToken: token => insertTokenRef.current(token),
|
|
187
241
|
expandToken: id => tokens.expandToken(id),
|
|
188
242
|
insertText: text => insertTextRef.current(text),
|
|
189
|
-
focus:
|
|
243
|
+
focus: focusEditable,
|
|
190
244
|
getValue: () => serialize(editableRef.current ?? document.createElement('div'))
|
|
191
245
|
};
|
|
192
246
|
selfRef.current = handle;
|
|
@@ -201,12 +255,12 @@ export function ChatComposerInput(props) {
|
|
|
201
255
|
return;
|
|
202
256
|
}
|
|
203
257
|
inputControlRef.current = {
|
|
204
|
-
focus:
|
|
258
|
+
focus: focusEditableAtEnd
|
|
205
259
|
};
|
|
206
260
|
return () => {
|
|
207
261
|
inputControlRef.current = null;
|
|
208
262
|
};
|
|
209
|
-
}, [inputControlRef]);
|
|
263
|
+
}, [inputControlRef, focusEditableAtEnd]);
|
|
210
264
|
useEffect(() => {
|
|
211
265
|
if (controlledValue === undefined || !editableRef.current) {
|
|
212
266
|
return;
|
|
@@ -373,12 +427,36 @@ export function ChatComposerInput(props) {
|
|
|
373
427
|
return;
|
|
374
428
|
}
|
|
375
429
|
|
|
376
|
-
// History navigation (only when trigger menu is not active)
|
|
430
|
+
// History navigation (only when trigger menu is not active).
|
|
431
|
+
// Recall only at the text boundaries so the caret can still move
|
|
432
|
+
// between lines in a multi-line draft: ArrowUp recalls the
|
|
433
|
+
// previous message when the caret is at the very start, ArrowDown
|
|
434
|
+
// steps forward when it's at the very end. A recalled message is
|
|
435
|
+
// shown fully selected (see `selectAll` below); that spans both
|
|
436
|
+
// boundaries at once, so repeated presses keep stepping through
|
|
437
|
+
// history. Mid-text, we bail before `preventDefault` and let the
|
|
438
|
+
// browser move the caret up/down a line.
|
|
377
439
|
if (hasHistory && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
|
378
440
|
if (!editableRef.current) {
|
|
379
441
|
return;
|
|
380
442
|
}
|
|
381
|
-
const
|
|
443
|
+
const editable = editableRef.current;
|
|
444
|
+
// Last-resort fallback for a caret we never placed: an engine that
|
|
445
|
+
// leaves no Range inside the editable on focus, or a consumer that
|
|
446
|
+
// focused the DOM node directly instead of through our focus
|
|
447
|
+
// control. Place it where the focus control would have, so a
|
|
448
|
+
// pending draft is never mistaken for a caret at the start. A no-op
|
|
449
|
+
// whenever a real caret exists — including one the user moved.
|
|
450
|
+
ensureCaretInside(editable);
|
|
451
|
+
const isCollapsed = window.getSelection()?.isCollapsed ?? true;
|
|
452
|
+
const atStart = isSelectionAtStart(editable);
|
|
453
|
+
const atEnd = isSelectionAtEnd(editable);
|
|
454
|
+
const canRecallPrev = atStart && (isCollapsed || atEnd);
|
|
455
|
+
const canRecallNext = atEnd && (isCollapsed || atStart);
|
|
456
|
+
if (e.key === 'ArrowUp' ? !canRecallPrev : !canRecallNext) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const text = serialize(editable);
|
|
382
460
|
const history = historyRef.current;
|
|
383
461
|
if (history.length === 0) {
|
|
384
462
|
return;
|
|
@@ -7,31 +7,93 @@
|
|
|
7
7
|
* Selection helpers for the contentEditable chat input. Both the
|
|
8
8
|
* imperative `insertToken` / `insertText` APIs and the paste pipeline
|
|
9
9
|
* need a valid Range inside the editable before they can mutate the
|
|
10
|
-
* DOM
|
|
11
|
-
* programmatically focused — a Range only exists once the user has
|
|
12
|
-
* clicked inside the editable or we've created one explicitly.
|
|
10
|
+
* DOM, and history recall needs to know where the caret sits.
|
|
13
11
|
*
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* What a programmatic `focus()` does to the Selection is browser- and
|
|
13
|
+
* state-dependent, and it is never what a click on the composer's padding
|
|
14
|
+
* means. Measured in Chromium: focusing a contentEditable collapses the
|
|
15
|
+
* caret to offset 0 — the *start* of the draft — whether or not a
|
|
16
|
+
* selection existed before, and whether the editable is empty or not.
|
|
17
|
+
* Other engines may instead leave no Range inside the editable at all.
|
|
18
|
+
*
|
|
19
|
+
* So the composer never infers the caret from a bare `focus()`:
|
|
20
|
+
* - `placeCaretAtEnd` states the caret outright, and is what the composer
|
|
21
|
+
* shell's click-to-focus uses, because clicking the space after a draft
|
|
22
|
+
* means "put me after the text" in every chat composer.
|
|
23
|
+
* - `getSelectionRangeInside` + `restoreSelectionRange` let the imperative
|
|
24
|
+
* `focus()` keep a caret or selection the user already had: it is read
|
|
25
|
+
* before focusing, since afterwards the engine's own caret is
|
|
26
|
+
* indistinguishable from theirs.
|
|
27
|
+
* - `ensureCaretInside` is the weaker fallback for callers that only
|
|
28
|
+
* need *a* valid Range (paste, imperative insertion): it respects a
|
|
29
|
+
* caret that already exists and creates one at the end otherwise.
|
|
18
30
|
*
|
|
19
31
|
* SYNC: When modified, update:
|
|
20
32
|
* - /packages/core/src/Chat/ChatComposerInput.tsx (consumer)
|
|
21
33
|
* - /packages/core/src/Chat/useChatComposerTokens.ts (consumer)
|
|
22
34
|
*/
|
|
35
|
+
/**
|
|
36
|
+
* The current Selection's range when it lies inside `editable`, or `null`.
|
|
37
|
+
*
|
|
38
|
+
* Callers use this to read the caret *before* focusing, because `focus()`
|
|
39
|
+
* creates a caret of its own and afterwards the two are indistinguishable.
|
|
40
|
+
* The range is cloned, so later DOM work cannot mutate the saved position.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getSelectionRangeInside(editable: HTMLElement): Range | null;
|
|
43
|
+
/** Make `range` the current Selection again. */
|
|
44
|
+
export declare function restoreSelectionRange(range: Range): void;
|
|
45
|
+
/**
|
|
46
|
+
* Collapse the Selection to the very end of `editable`'s content,
|
|
47
|
+
* replacing whatever the Selection held before.
|
|
48
|
+
*
|
|
49
|
+
* Unlike {@link ensureCaretInside} this is unconditional: a caret the
|
|
50
|
+
* browser placed on `focus()` is overwritten, which is the point — that
|
|
51
|
+
* caret lands at the start of the draft, where ArrowUp means "recall
|
|
52
|
+
* history" and would discard what the user had typed.
|
|
53
|
+
*
|
|
54
|
+
* Returns `true` when the caret was placed, `false` if the Selection API
|
|
55
|
+
* is unavailable (e.g. a detached document).
|
|
56
|
+
*/
|
|
57
|
+
export declare function placeCaretAtEnd(editable: HTMLElement): boolean;
|
|
23
58
|
/**
|
|
24
59
|
* Ensure the current Selection has a Range inside `editable`.
|
|
25
60
|
*
|
|
26
61
|
* If `window.getSelection()` already has a Range whose `startContainer`
|
|
27
|
-
* is inside `editable`, this is a no-op
|
|
28
|
-
*
|
|
62
|
+
* is inside `editable`, this is a no-op — including the start-of-draft
|
|
63
|
+
* caret a `focus()` leaves behind, which is why callers that care where
|
|
64
|
+
* the caret lands want {@link placeCaretAtEnd} instead. Otherwise a
|
|
65
|
+
* collapsed caret is placed at the end of `editable`.
|
|
29
66
|
*
|
|
30
67
|
* Returns the live `Selection` on success, or `null` if no Selection
|
|
31
68
|
* is available at all (e.g. detached document, JSDOM without selection
|
|
32
69
|
* support).
|
|
33
70
|
*/
|
|
34
71
|
export declare function ensureCaretInside(editable: HTMLElement): Selection | null;
|
|
72
|
+
/**
|
|
73
|
+
* Whether the current selection's start boundary sits at the very
|
|
74
|
+
* beginning of `editable`'s content.
|
|
75
|
+
*
|
|
76
|
+
* Used by the composer to decide when ArrowUp should recall message
|
|
77
|
+
* history versus move the caret up a line: history is only recalled
|
|
78
|
+
* when the caret is at the very start of the draft. This handles both
|
|
79
|
+
* the editable root and offset 0 of its leading text node without
|
|
80
|
+
* cloning the draft's contents.
|
|
81
|
+
*
|
|
82
|
+
* Returns `false` when there is no selection, the range is not inside
|
|
83
|
+
* `editable`, or the boundary APIs are unavailable — callers treat
|
|
84
|
+
* that as "not at the boundary" and let the browser move the caret.
|
|
85
|
+
*/
|
|
86
|
+
export declare function isSelectionAtStart(editable: HTMLElement): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Whether the current selection's end boundary sits at the very end of
|
|
89
|
+
* `editable`'s content.
|
|
90
|
+
*
|
|
91
|
+
* The ArrowDown counterpart to {@link isSelectionAtStart}: history is
|
|
92
|
+
* only stepped forward when the caret is at the very end of the draft.
|
|
93
|
+
*
|
|
94
|
+
* Returns `false` under the same conditions as {@link isSelectionAtStart}.
|
|
95
|
+
*/
|
|
96
|
+
export declare function isSelectionAtEnd(editable: HTMLElement): boolean;
|
|
35
97
|
/**
|
|
36
98
|
* Insert plain text at the current selection, scoped to `editable`.
|
|
37
99
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatComposerSelection.d.ts","sourceRoot":"","sources":["../../src/Chat/chatComposerSelection.ts"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"chatComposerSelection.d.ts","sourceRoot":"","sources":["../../src/Chat/chatComposerSelection.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,WAAW,GAAG,KAAK,GAAG,IAAI,CAa3E;AAED,gDAAgD;AAChD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAOxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAW9D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAmBzE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAYjE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAO/D;AAiED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,WAAW,EACrB,IAAI,EAAE,MAAM,GACX,OAAO,CAiBT"}
|
|
@@ -11,26 +11,94 @@
|
|
|
11
11
|
* Selection helpers for the contentEditable chat input. Both the
|
|
12
12
|
* imperative `insertToken` / `insertText` APIs and the paste pipeline
|
|
13
13
|
* need a valid Range inside the editable before they can mutate the
|
|
14
|
-
* DOM
|
|
15
|
-
* programmatically focused — a Range only exists once the user has
|
|
16
|
-
* clicked inside the editable or we've created one explicitly.
|
|
14
|
+
* DOM, and history recall needs to know where the caret sits.
|
|
17
15
|
*
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* What a programmatic `focus()` does to the Selection is browser- and
|
|
17
|
+
* state-dependent, and it is never what a click on the composer's padding
|
|
18
|
+
* means. Measured in Chromium: focusing a contentEditable collapses the
|
|
19
|
+
* caret to offset 0 — the *start* of the draft — whether or not a
|
|
20
|
+
* selection existed before, and whether the editable is empty or not.
|
|
21
|
+
* Other engines may instead leave no Range inside the editable at all.
|
|
22
|
+
*
|
|
23
|
+
* So the composer never infers the caret from a bare `focus()`:
|
|
24
|
+
* - `placeCaretAtEnd` states the caret outright, and is what the composer
|
|
25
|
+
* shell's click-to-focus uses, because clicking the space after a draft
|
|
26
|
+
* means "put me after the text" in every chat composer.
|
|
27
|
+
* - `getSelectionRangeInside` + `restoreSelectionRange` let the imperative
|
|
28
|
+
* `focus()` keep a caret or selection the user already had: it is read
|
|
29
|
+
* before focusing, since afterwards the engine's own caret is
|
|
30
|
+
* indistinguishable from theirs.
|
|
31
|
+
* - `ensureCaretInside` is the weaker fallback for callers that only
|
|
32
|
+
* need *a* valid Range (paste, imperative insertion): it respects a
|
|
33
|
+
* caret that already exists and creates one at the end otherwise.
|
|
22
34
|
*
|
|
23
35
|
* SYNC: When modified, update:
|
|
24
36
|
* - /packages/core/src/Chat/ChatComposerInput.tsx (consumer)
|
|
25
37
|
* - /packages/core/src/Chat/useChatComposerTokens.ts (consumer)
|
|
26
38
|
*/
|
|
27
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The current Selection's range when it lies inside `editable`, or `null`.
|
|
42
|
+
*
|
|
43
|
+
* Callers use this to read the caret *before* focusing, because `focus()`
|
|
44
|
+
* creates a caret of its own and afterwards the two are indistinguishable.
|
|
45
|
+
* The range is cloned, so later DOM work cannot mutate the saved position.
|
|
46
|
+
*/
|
|
47
|
+
export function getSelectionRangeInside(editable) {
|
|
48
|
+
const selection = window.getSelection();
|
|
49
|
+
if (!selection || selection.rangeCount === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const range = selection.getRangeAt(0);
|
|
53
|
+
if (!editable.contains(range.startContainer) || !editable.contains(range.endContainer)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return range.cloneRange();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Make `range` the current Selection again. */
|
|
60
|
+
export function restoreSelectionRange(range) {
|
|
61
|
+
const selection = window.getSelection();
|
|
62
|
+
if (!selection) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
selection.removeAllRanges();
|
|
66
|
+
selection.addRange(range);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Collapse the Selection to the very end of `editable`'s content,
|
|
71
|
+
* replacing whatever the Selection held before.
|
|
72
|
+
*
|
|
73
|
+
* Unlike {@link ensureCaretInside} this is unconditional: a caret the
|
|
74
|
+
* browser placed on `focus()` is overwritten, which is the point — that
|
|
75
|
+
* caret lands at the start of the draft, where ArrowUp means "recall
|
|
76
|
+
* history" and would discard what the user had typed.
|
|
77
|
+
*
|
|
78
|
+
* Returns `true` when the caret was placed, `false` if the Selection API
|
|
79
|
+
* is unavailable (e.g. a detached document).
|
|
80
|
+
*/
|
|
81
|
+
export function placeCaretAtEnd(editable) {
|
|
82
|
+
const selection = window.getSelection();
|
|
83
|
+
if (!selection) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const range = document.createRange();
|
|
87
|
+
range.selectNodeContents(editable);
|
|
88
|
+
range.collapse(false); // collapse to end
|
|
89
|
+
selection.removeAllRanges();
|
|
90
|
+
selection.addRange(range);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
28
94
|
/**
|
|
29
95
|
* Ensure the current Selection has a Range inside `editable`.
|
|
30
96
|
*
|
|
31
97
|
* If `window.getSelection()` already has a Range whose `startContainer`
|
|
32
|
-
* is inside `editable`, this is a no-op
|
|
33
|
-
*
|
|
98
|
+
* is inside `editable`, this is a no-op — including the start-of-draft
|
|
99
|
+
* caret a `focus()` leaves behind, which is why callers that care where
|
|
100
|
+
* the caret lands want {@link placeCaretAtEnd} instead. Otherwise a
|
|
101
|
+
* collapsed caret is placed at the end of `editable`.
|
|
34
102
|
*
|
|
35
103
|
* Returns the live `Selection` on success, or `null` if no Selection
|
|
36
104
|
* is available at all (e.g. detached document, JSDOM without selection
|
|
@@ -55,6 +123,84 @@ export function ensureCaretInside(editable) {
|
|
|
55
123
|
return selection;
|
|
56
124
|
}
|
|
57
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Whether the current selection's start boundary sits at the very
|
|
128
|
+
* beginning of `editable`'s content.
|
|
129
|
+
*
|
|
130
|
+
* Used by the composer to decide when ArrowUp should recall message
|
|
131
|
+
* history versus move the caret up a line: history is only recalled
|
|
132
|
+
* when the caret is at the very start of the draft. This handles both
|
|
133
|
+
* the editable root and offset 0 of its leading text node without
|
|
134
|
+
* cloning the draft's contents.
|
|
135
|
+
*
|
|
136
|
+
* Returns `false` when there is no selection, the range is not inside
|
|
137
|
+
* `editable`, or the boundary APIs are unavailable — callers treat
|
|
138
|
+
* that as "not at the boundary" and let the browser move the caret.
|
|
139
|
+
*/
|
|
140
|
+
export function isSelectionAtStart(editable) {
|
|
141
|
+
const selection = window.getSelection();
|
|
142
|
+
if (!selection || selection.rangeCount === 0) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const range = selection.getRangeAt(0);
|
|
146
|
+
return isBoundaryAtEdge(editable, range.startContainer, range.startOffset, 'start');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether the current selection's end boundary sits at the very end of
|
|
151
|
+
* `editable`'s content.
|
|
152
|
+
*
|
|
153
|
+
* The ArrowDown counterpart to {@link isSelectionAtStart}: history is
|
|
154
|
+
* only stepped forward when the caret is at the very end of the draft.
|
|
155
|
+
*
|
|
156
|
+
* Returns `false` under the same conditions as {@link isSelectionAtStart}.
|
|
157
|
+
*/
|
|
158
|
+
export function isSelectionAtEnd(editable) {
|
|
159
|
+
const selection = window.getSelection();
|
|
160
|
+
if (!selection || selection.rangeCount === 0) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const range = selection.getRangeAt(0);
|
|
164
|
+
return isBoundaryAtEdge(editable, range.endContainer, range.endOffset, 'end');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check whether a Range boundary is at an edge of `editable` without
|
|
169
|
+
* materializing the content before or after it. Chromium represents a caret
|
|
170
|
+
* at the start of non-empty contenteditables as the first text node at offset
|
|
171
|
+
* zero (and likewise uses the last text node's length at the end). Those are
|
|
172
|
+
* distinct DOM boundary points from the editable's child offsets, so
|
|
173
|
+
* `compareBoundaryPoints` cannot be used here.
|
|
174
|
+
*/
|
|
175
|
+
function isBoundaryAtEdge(editable, container, offset, edge) {
|
|
176
|
+
if (!editable.contains(container) || !isAtNodeEdge(container, offset, edge)) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
for (let node = container; node && node !== editable;) {
|
|
180
|
+
if (hasContentSibling(node, edge)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
node = node.parentNode;
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
function isAtNodeEdge(node, offset, edge) {
|
|
188
|
+
if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.CDATA_SECTION_NODE) {
|
|
189
|
+
return edge === 'start' ? offset === 0 : offset === (node.nodeValue?.length ?? 0);
|
|
190
|
+
}
|
|
191
|
+
return edge === 'start' ? offset === 0 : offset === node.childNodes.length;
|
|
192
|
+
}
|
|
193
|
+
function hasContentSibling(node, edge) {
|
|
194
|
+
for (let sibling = edge === 'start' ? node.previousSibling : node.nextSibling; sibling; sibling = edge === 'start' ? sibling.previousSibling : sibling.nextSibling) {
|
|
195
|
+
// Elements (including <br> and tokens) represent a content boundary even
|
|
196
|
+
// when they have no text. Empty text nodes and comments do not.
|
|
197
|
+
if (sibling.nodeType === Node.ELEMENT_NODE || sibling.nodeType === Node.TEXT_NODE && sibling.nodeValue !== '') {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
58
204
|
/**
|
|
59
205
|
* Insert plain text at the current selection, scoped to `editable`.
|
|
60
206
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useChatStreamScroll.d.ts","sourceRoot":"","sources":["../../src/Chat/useChatStreamScroll.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useChatStreamScroll.d.ts","sourceRoot":"","sources":["../../src/Chat/useChatStreamScroll.ts"],"names":[],"mappings":"AAsCA,MAAM,WAAW,yBAAyB;IACxC;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;CACjC;AAED,MAAM,WAAW,0BAA0B;IACzC;;OAEG;IACH,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE/C;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,yBAAyB;IACxC,6DAA6D;IAC7D,YAAY,EAAE,OAAO,CAAC;IAEtB,yDAAyD;IACzD,QAAQ,EAAE,OAAO,CAAC;IAElB,yDAAyD;IACzD,cAAc,EAAE,CAAC,OAAO,CAAC,EAAE,yBAAyB,KAAK,IAAI,CAAC;IAE9D,sFAAsF;IACtF,eAAe,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;IAE3C,6CAA6C;IAC7C,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB,0BAA0B;IAC1B,MAAM,EAAE,MAAM,IAAI,CAAC;IAEnB,oEAAoE;IACpE,cAAc,EAAE,MAAM,IAAI,CAAC;IAE3B,mDAAmD;IACnD,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC;AA8BD,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,OAAc,EACd,aAAkB,EAClB,eAAqB,EACrB,OAAa,EACb,SAAgB,EAChB,IAAW,GACZ,EAAE,0BAA0B,GAAG,yBAAyB,CAqQxD"}
|