one-design-next 0.0.113 → 0.0.114
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/_genui-types.d.ts +42 -3
- package/dist/_genui-types.js +19 -3
- package/dist/collapse/primitive.d.ts +1 -1
- package/dist/composer/caret.d.ts +74 -0
- package/dist/composer/caret.js +207 -1
- package/dist/composer/editor.d.ts +4 -3
- package/dist/composer/editor.js +292 -18
- package/dist/composer/hooks/useChipManager.d.ts +10 -2
- package/dist/composer/hooks/useChipManager.js +47 -14
- package/dist/composer/index.d.ts +6 -0
- package/dist/composer/index.js +35 -6
- package/dist/composer/inline-ref.d.ts +6 -1
- package/dist/composer/inline-ref.js +6 -1
- package/dist/composer/segments.d.ts +32 -4
- package/dist/composer/segments.js +86 -5
- package/dist/composer/style/index.css +22 -0
- package/dist/composer/utils.d.ts +21 -1
- package/dist/composer/utils.js +47 -3
- package/dist/icon/types.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/user-bubble/render-segments.d.ts +1 -1
- package/dist/user-bubble/render-segments.js +11 -2
- package/dist/user-bubble/style/index.css +8 -0
- package/package.json +1 -1
package/dist/_genui-types.d.ts
CHANGED
|
@@ -120,7 +120,8 @@ export interface MentionData {
|
|
|
120
120
|
}
|
|
121
121
|
/**
|
|
122
122
|
* Composer 编辑器内 chip 实例数据(DOM host + Portal 渲染用)。
|
|
123
|
-
* 发送时经 `rawValueToSegments` 转为 `ComposerSegment` 的 invocation / mention
|
|
123
|
+
* 发送时经 `rawValueToSegments` 转为 `ComposerSegment` 的 invocation / mention,
|
|
124
|
+
* 并在命中结构谓词时追加一段 `hint`(见 `ChipData.hint`)。
|
|
124
125
|
*/
|
|
125
126
|
export interface ChipData {
|
|
126
127
|
/** chip 实例唯一 id(与 value marker 中的 id 对应) */
|
|
@@ -136,12 +137,30 @@ export interface ChipData {
|
|
|
136
137
|
kind?: 'invocation' | 'mention' | (string & NonNullable<unknown>);
|
|
137
138
|
state?: InlineRefState;
|
|
138
139
|
payload?: Record<string, unknown>;
|
|
140
|
+
/**
|
|
141
|
+
* 提示文本快照(插入时由 `SkillItem.hint` 拷贝进来)。
|
|
142
|
+
*
|
|
143
|
+
* **不是正文内容**:既不进 `value`(无 marker),也不进剪贴板;只在
|
|
144
|
+
* 「结构谓词成立」时作为派生的只读视觉渲染在 chip 之后——即该 chip 是
|
|
145
|
+
* 最后一个内容、且其后**一个字符都没有**(空格 / 换行都算内容;详见
|
|
146
|
+
* `resolveHintTarget`)。
|
|
147
|
+
* 发送时由 `rawValueToSegments` 按同一谓词追加为 `ComposerHintSegment`。
|
|
148
|
+
*/
|
|
149
|
+
hint?: string;
|
|
139
150
|
}
|
|
140
151
|
export interface SkillItem {
|
|
141
152
|
id: string;
|
|
142
153
|
label: string;
|
|
143
154
|
icon: string;
|
|
144
155
|
description?: string;
|
|
156
|
+
/**
|
|
157
|
+
* skill 的提示文本(如「根据近期投放数据生成复盘报告…」这类模板文案)。
|
|
158
|
+
*
|
|
159
|
+
* 选中插入后,只要该 skill chip 是输入框里最后一个内容且其后没有其它内容
|
|
160
|
+
* (空格 / 换行都算内容),就会以只读灰字形式渲染在 chip 后面;用户在其后
|
|
161
|
+
* 输入任何字符或换行即自动隐藏。与光标位置无关。
|
|
162
|
+
*/
|
|
163
|
+
hint?: string;
|
|
145
164
|
/**
|
|
146
165
|
* 插入后的默认视觉态。`'pending'` 时若 Composer 提供 `renderParamPanel`,
|
|
147
166
|
* 插入后会自动打开参数面板。
|
|
@@ -166,8 +185,11 @@ export interface SkillGroup {
|
|
|
166
185
|
* 这是 `onSend` 的**主出口**:业务按此数组顺序自定义拼后端(可选用
|
|
167
186
|
* `segmentsToReadableText` 工具函数兜底拼成一句话)。
|
|
168
187
|
*
|
|
169
|
-
*
|
|
188
|
+
* 各分支拆成命名 interface 导出,业务可直接 `import` 单一形态,
|
|
170
189
|
* 无需再写 `Extract<ComposerSegment, { type: 'xxx' }>`。
|
|
190
|
+
*
|
|
191
|
+
* 其中 `hint` 段是**派生**的(非用户输入),仅在最后一个 skill chip 之后无内容时
|
|
192
|
+
* 由 Composer 追加;其余三种都来自用户输入。
|
|
171
193
|
*/
|
|
172
194
|
/** Composer 文本片段携带的数据。 */
|
|
173
195
|
export interface TextData {
|
|
@@ -184,6 +206,8 @@ export declare const ComposerSegmentType: {
|
|
|
184
206
|
readonly Text: "text";
|
|
185
207
|
readonly Invocation: "invocation";
|
|
186
208
|
readonly Mention: "mention";
|
|
209
|
+
/** 派生的 skill 提示文本;发送时按结构谓词追加,见 `ComposerHintSegment` */
|
|
210
|
+
readonly Hint: "hint";
|
|
187
211
|
};
|
|
188
212
|
export type ComposerSegmentType = (typeof ComposerSegmentType)[keyof typeof ComposerSegmentType];
|
|
189
213
|
export interface ComposerTextSegment {
|
|
@@ -198,7 +222,22 @@ export interface ComposerMentionSegment {
|
|
|
198
222
|
type: typeof ComposerSegmentType.Mention;
|
|
199
223
|
data: MentionData;
|
|
200
224
|
}
|
|
201
|
-
|
|
225
|
+
/**
|
|
226
|
+
* 派生的 skill 提示段:**不是用户输入的内容**,而是「最后一个 skill chip + 其后
|
|
227
|
+
* 无内容」这一结构谓词成立时,由 Composer 在发送瞬间追加的模板文案。
|
|
228
|
+
*
|
|
229
|
+
* 只在 `onSend` 的 segments 里出现;不出现在 `value` / `chips` / 剪贴板里。
|
|
230
|
+
* 需要忽略它的业务可以直接 filter,或用 `isHintSegment` 收窄。
|
|
231
|
+
*/
|
|
232
|
+
export interface ComposerHintSegment {
|
|
233
|
+
type: typeof ComposerSegmentType.Hint;
|
|
234
|
+
data: HintData;
|
|
235
|
+
}
|
|
236
|
+
/** Composer 派生 hint 段携带的数据。 */
|
|
237
|
+
export interface HintData {
|
|
238
|
+
text: string;
|
|
239
|
+
}
|
|
240
|
+
export type ComposerSegment = ComposerTextSegment | ComposerInvocationSegment | ComposerMentionSegment | ComposerHintSegment;
|
|
202
241
|
/** `onSend(segments, meta?)` 第二参数:非正文的附加数据。 */
|
|
203
242
|
export interface SendMeta {
|
|
204
243
|
attachments?: Attachment[];
|
package/dist/_genui-types.js
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Composer 编辑器内 chip 实例数据(DOM host + Portal 渲染用)。
|
|
19
|
-
* 发送时经 `rawValueToSegments` 转为 `ComposerSegment` 的 invocation / mention
|
|
19
|
+
* 发送时经 `rawValueToSegments` 转为 `ComposerSegment` 的 invocation / mention,
|
|
20
|
+
* 并在命中结构谓词时追加一段 `hint`(见 `ChipData.hint`)。
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
/** 技能分组,用于在 SkillSlot menu 变体中按组名分隔展示。 */
|
|
@@ -27,8 +28,11 @@
|
|
|
27
28
|
* 这是 `onSend` 的**主出口**:业务按此数组顺序自定义拼后端(可选用
|
|
28
29
|
* `segmentsToReadableText` 工具函数兜底拼成一句话)。
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
+
* 各分支拆成命名 interface 导出,业务可直接 `import` 单一形态,
|
|
31
32
|
* 无需再写 `Extract<ComposerSegment, { type: 'xxx' }>`。
|
|
33
|
+
*
|
|
34
|
+
* 其中 `hint` 段是**派生**的(非用户输入),仅在最后一个 skill chip 之后无内容时
|
|
35
|
+
* 由 Composer 追加;其余三种都来自用户输入。
|
|
32
36
|
*/
|
|
33
37
|
|
|
34
38
|
/** Composer 文本片段携带的数据。 */
|
|
@@ -43,9 +47,21 @@
|
|
|
43
47
|
export var ComposerSegmentType = {
|
|
44
48
|
Text: 'text',
|
|
45
49
|
Invocation: 'invocation',
|
|
46
|
-
Mention: 'mention'
|
|
50
|
+
Mention: 'mention',
|
|
51
|
+
/** 派生的 skill 提示文本;发送时按结构谓词追加,见 `ComposerHintSegment` */
|
|
52
|
+
Hint: 'hint'
|
|
47
53
|
};
|
|
48
54
|
// 同名 type 供 `seg.type === ComposerSegmentType.Invocation` 收窄,与 const 对象并存。
|
|
49
55
|
// eslint-disable-next-line @typescript-eslint/no-redeclare -- 有意的 const + type 同名
|
|
50
56
|
|
|
57
|
+
/**
|
|
58
|
+
* 派生的 skill 提示段:**不是用户输入的内容**,而是「最后一个 skill chip + 其后
|
|
59
|
+
* 无内容」这一结构谓词成立时,由 Composer 在发送瞬间追加的模板文案。
|
|
60
|
+
*
|
|
61
|
+
* 只在 `onSend` 的 segments 里出现;不出现在 `value` / `chips` / 剪贴板里。
|
|
62
|
+
* 需要忽略它的业务可以直接 filter,或用 `isHintSegment` 收窄。
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/** Composer 派生 hint 段携带的数据。 */
|
|
66
|
+
|
|
51
67
|
/** `onSend(segments, meta?)` 第二参数:非正文的附加数据。 */
|
|
@@ -25,7 +25,7 @@ type CollapsibleContentProps = Omit<HTMLMotionProps<'div'>, 'children' | 'ref' |
|
|
|
25
25
|
children?: React.ReactNode;
|
|
26
26
|
transition?: Transition;
|
|
27
27
|
};
|
|
28
|
-
declare const CollapsibleContent: React.ForwardRefExoticComponent<Omit<HTMLMotionProps<"div">, "
|
|
28
|
+
declare const CollapsibleContent: React.ForwardRefExoticComponent<Omit<HTMLMotionProps<"div">, "ref" | "children" | "transition"> & {
|
|
29
29
|
children?: React.ReactNode;
|
|
30
30
|
transition?: Transition | undefined;
|
|
31
31
|
} & React.RefAttributes<HTMLDivElement>>;
|
package/dist/composer/caret.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** hint 左侧的落点:优先用紧邻的文本节点尾,否则退化为「hint 之前的元素偏移」。 */
|
|
2
|
+
export declare function getCaretBeforeHint(hint: HTMLElement): {
|
|
3
|
+
node: Node;
|
|
4
|
+
offset: number;
|
|
5
|
+
} | null;
|
|
1
6
|
export declare function getCurrentLine(node: Node | null | undefined, editor: HTMLElement): HTMLDivElement | null;
|
|
2
7
|
export declare function findChipHostBeforeCaret(range: Range): HTMLElement | null;
|
|
3
8
|
export declare function findChipHostAfterCaret(range: Range): HTMLElement | null;
|
|
@@ -5,6 +10,75 @@ export declare function normalizeCollapsedCaretAtChipBoundary(range: Range, edit
|
|
|
5
10
|
node: Node;
|
|
6
11
|
offset: number;
|
|
7
12
|
} | null;
|
|
13
|
+
/**
|
|
14
|
+
* 给定插入点是否位于 hint「之后」(含落在 hint 内部)。
|
|
15
|
+
* mousedown 拦截、caret 归位都要用它,因此不含任何 selection 依赖。
|
|
16
|
+
*
|
|
17
|
+
* 必须处理**祖先节点**:点在线 / 编辑器留白处时,`caretRangeFromPoint` 常返回
|
|
18
|
+
* (行容器, n) 甚至 (editor, n);对这些节点 `compareDocumentPosition` 只会给
|
|
19
|
+
* CONTAINS / PRECEDING,判断不出「在 hint 之后」。
|
|
20
|
+
*/
|
|
21
|
+
export declare function isPositionAfterHint(node: Node, offset: number, hint: HTMLElement): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* 点击坐标是否落在 hint 的任一视觉行上(hint 折行时每行一个 rect)。
|
|
24
|
+
*
|
|
25
|
+
* hint 是 `pointer-events: none` 的只读派生文本,原生对「点在不可编辑内联元素上」
|
|
26
|
+
* 的 caret 解析没有稳定契约:可能给到 hint 内部、行首、hint 之后……只要给的不是
|
|
27
|
+
* 「hint 之后」,就会漏掉拦截,先画一帧错位置再被 selectionchange 拉回。
|
|
28
|
+
* 坐标命中不依赖这套启发式,是「用户就是想点 hint」最直接的判据。
|
|
29
|
+
*
|
|
30
|
+
* 容差 1px:rect 是小数,点击坐标是整数,贴着边缘点也算命中。
|
|
31
|
+
*/
|
|
32
|
+
export declare function isPointOnHint(x: number, y: number, hint: HTMLElement): boolean;
|
|
33
|
+
/** 取点击坐标对应的插入点。Chrome / Safari 用 `caretRangeFromPoint`,Firefox 用 `caretPositionFromPoint`。 */
|
|
34
|
+
export declare function caretRangeFromPoint(x: number, y: number): Range | null;
|
|
35
|
+
/**
|
|
36
|
+
* caret 是否已经「贴在 hint 左侧」——caret 到 hint 之间除了 chip / 光标锚点
|
|
37
|
+
* (ZWSP)之外没有别的内容。
|
|
38
|
+
*
|
|
39
|
+
* 用于拦截 ArrowRight:再往右一步就会跨到 hint 之后(哪怕只是一帧也会闪),
|
|
40
|
+
* 所以这里不能只认「紧邻 hint 的文本末尾」,要认「右侧已无正文」的所有位置。
|
|
41
|
+
*/
|
|
42
|
+
export declare function isCaretBeforeHint(range: Range, editor: HTMLElement): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* 派生 hint 之后不是合法插入点:hint 是只读派生文本,caret 停在它后面会让用户
|
|
45
|
+
* 误以为能在提示文案末尾继续输入(且视觉上离 chip 很远)。
|
|
46
|
+
*
|
|
47
|
+
* 命中时把 caret 拉回 hint 之前 —— 也就是「chip 右侧」这个唯一正确的落点。
|
|
48
|
+
* 覆盖点击末尾、End、ArrowRight / ArrowDown 落到行尾等所有路径。
|
|
49
|
+
*/
|
|
50
|
+
export declare function normalizeCollapsedCaretBeforeHint(range: Range, editor: HTMLElement): {
|
|
51
|
+
node: Node;
|
|
52
|
+
offset: number;
|
|
53
|
+
} | null;
|
|
54
|
+
/**
|
|
55
|
+
* 查找 caret 所在行的「下一行」是否就是 hint 所在行 —— 用于拦截 ArrowDown:
|
|
56
|
+
* 该行除 chip + hint 外没有别的内容,唯二落点里只有 hint 之前合法,
|
|
57
|
+
* 所以直接落到那里,避免原生移动先跳到行尾(hint 之后)再被拉回的一帧闪烁。
|
|
58
|
+
*/
|
|
59
|
+
export declare function isHintOnNextLine(range: Range, editor: HTMLElement): boolean;
|
|
60
|
+
/** caret 是否已经在 hint 所在行上(End 会跳到该行行尾 = hint 之后)。 */
|
|
61
|
+
export declare function isCaretOnHintLine(range: Range, editor: HTMLElement): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* 上下方向键的落点是否会跑到 hint 上 / 之后。
|
|
64
|
+
*
|
|
65
|
+
* 两种形态都要覆盖:
|
|
66
|
+
* 1. hint 在相邻的 line div(↓ 时判定下一行,↑ 时判定上一行);
|
|
67
|
+
* 2. hint 与自己**同一个 line div 内软换行**(长文案折行,demo 的常态)→
|
|
68
|
+
* 按几何判定:hint 的任一视觉行落在「caret 上/下方一行」的区间里,
|
|
69
|
+
* 方向键就必然落进提示文案区域(那里没有可编辑落点,浏览器会给到 hint 之后)。
|
|
70
|
+
*
|
|
71
|
+
* 不用 `caretRangeFromPoint` 探测:hint 是 contentEditable=false,浏览器会把
|
|
72
|
+
* 该点解析成它前面最近的编辑位,探测结果反过来是「没越界」,靠不住。
|
|
73
|
+
*
|
|
74
|
+
* ↑ 只在 caret **不在** hint 所在行时才判定:同一条上按 ↑ 是正常上移。
|
|
75
|
+
*/
|
|
76
|
+
export declare function wouldArrowCrossHint(range: Range, editor: HTMLElement, direction: 'up' | 'down'): boolean;
|
|
77
|
+
/** 直接取当前 hint 元素的左侧落点(调用方负责写入 selection)。 */
|
|
78
|
+
export declare function caretBeforeHintIn(editor: HTMLElement): {
|
|
79
|
+
node: Node;
|
|
80
|
+
offset: number;
|
|
81
|
+
} | null;
|
|
8
82
|
export declare function resolveCaretMove(range: Range, direction: 'left' | 'right' | 'up' | 'down', editor: HTMLElement): {
|
|
9
83
|
node: Node;
|
|
10
84
|
offset: number;
|
package/dist/composer/caret.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createLineRoot, ensureLineRoots as ensureLineRootsInUtils, getCurrentLineRoot, isLineRoot, LINE_ATTR_NAME, stripInvisibleChars, ZWSP } from "./utils";
|
|
1
|
+
import { COMPOSER_HINT_SELECTOR, createLineRoot, ensureLineRoots as ensureLineRootsInUtils, getCurrentLineRoot, isLineRoot, LINE_ATTR_NAME, stripInvisibleChars, ZWSP } from "./utils";
|
|
2
2
|
var CHIP_SELECTOR = '[data-odn-composer-chip-host],[data-mention-id]';
|
|
3
3
|
var isChipHost = function isChipHost(node) {
|
|
4
4
|
return !!node && node.nodeType === Node.ELEMENT_NODE && node instanceof HTMLElement && (node.hasAttribute('data-odn-composer-chip-host') || node.dataset.mentionId !== undefined);
|
|
@@ -120,10 +120,38 @@ var caretAfterNode = function caretAfterNode(target) {
|
|
|
120
120
|
var idx = childIndex(target);
|
|
121
121
|
return createTextCaretAtBoundary(parent, idx + 1, 'right');
|
|
122
122
|
};
|
|
123
|
+
|
|
124
|
+
/** hint 左侧的落点:优先用紧邻的文本节点尾,否则退化为「hint 之前的元素偏移」。 */
|
|
125
|
+
export function getCaretBeforeHint(hint) {
|
|
126
|
+
var _parent$childNodes2;
|
|
127
|
+
var parent = hint.parentNode;
|
|
128
|
+
if (!parent) return null;
|
|
129
|
+
var idx = childIndex(hint);
|
|
130
|
+
var prev = (_parent$childNodes2 = parent.childNodes[idx - 1]) !== null && _parent$childNodes2 !== void 0 ? _parent$childNodes2 : null;
|
|
131
|
+
if (prev && prev.nodeType === Node.TEXT_NODE) {
|
|
132
|
+
var _prev$textContent;
|
|
133
|
+
return {
|
|
134
|
+
node: prev,
|
|
135
|
+
offset: ((_prev$textContent = prev.textContent) !== null && _prev$textContent !== void 0 ? _prev$textContent : '').length
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
node: parent,
|
|
140
|
+
offset: idx
|
|
141
|
+
};
|
|
142
|
+
}
|
|
123
143
|
var lineStartCaret = function lineStartCaret(line) {
|
|
124
144
|
return createTextCaretAtBoundary(line, 0, 'right');
|
|
125
145
|
};
|
|
126
146
|
var lineEndCaret = function lineEndCaret(line) {
|
|
147
|
+
/* 行尾是派生 hint 时,「行尾」不是合法落点:hint 之后不允许停 caret。
|
|
148
|
+
* 若直接走 createTextCaretAtBoundary,会因为行尾没有 text 节点而在 hint
|
|
149
|
+
* 之后新建锚点 —— 跨行移动(← 从下一行行首回来)正好命中这条路径。 */
|
|
150
|
+
var hint = line.querySelector(COMPOSER_HINT_SELECTOR);
|
|
151
|
+
if (hint && hint.parentElement === line) {
|
|
152
|
+
var beforeHint = getCaretBeforeHint(hint);
|
|
153
|
+
if (beforeHint) return beforeHint;
|
|
154
|
+
}
|
|
127
155
|
return createTextCaretAtBoundary(line, line.childNodes.length, 'left');
|
|
128
156
|
};
|
|
129
157
|
var hasContentBeforeCaretInLine = function hasContentBeforeCaretInLine(range, line) {
|
|
@@ -231,6 +259,184 @@ export function normalizeCollapsedCaretAtChipBoundary(range, editor) {
|
|
|
231
259
|
var prefer = leftChip && !rightChip ? 'right' : 'left';
|
|
232
260
|
return createTextCaretAtBoundary(node, safeOffset, prefer);
|
|
233
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 给定插入点是否位于 hint「之后」(含落在 hint 内部)。
|
|
265
|
+
* mousedown 拦截、caret 归位都要用它,因此不含任何 selection 依赖。
|
|
266
|
+
*
|
|
267
|
+
* 必须处理**祖先节点**:点在线 / 编辑器留白处时,`caretRangeFromPoint` 常返回
|
|
268
|
+
* (行容器, n) 甚至 (editor, n);对这些节点 `compareDocumentPosition` 只会给
|
|
269
|
+
* CONTAINS / PRECEDING,判断不出「在 hint 之后」。
|
|
270
|
+
*/
|
|
271
|
+
export function isPositionAfterHint(node, offset, hint) {
|
|
272
|
+
if (node === hint) return offset > 0;
|
|
273
|
+
if (hint.contains(node)) return true;
|
|
274
|
+
if (node.contains(hint)) {
|
|
275
|
+
var child = hint;
|
|
276
|
+
while (child.parentNode && child.parentNode !== node) {
|
|
277
|
+
child = child.parentNode;
|
|
278
|
+
}
|
|
279
|
+
if (child.parentNode !== node) return false;
|
|
280
|
+
return offset > childIndex(child);
|
|
281
|
+
}
|
|
282
|
+
return !!(hint.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 点击坐标是否落在 hint 的任一视觉行上(hint 折行时每行一个 rect)。
|
|
287
|
+
*
|
|
288
|
+
* hint 是 `pointer-events: none` 的只读派生文本,原生对「点在不可编辑内联元素上」
|
|
289
|
+
* 的 caret 解析没有稳定契约:可能给到 hint 内部、行首、hint 之后……只要给的不是
|
|
290
|
+
* 「hint 之后」,就会漏掉拦截,先画一帧错位置再被 selectionchange 拉回。
|
|
291
|
+
* 坐标命中不依赖这套启发式,是「用户就是想点 hint」最直接的判据。
|
|
292
|
+
*
|
|
293
|
+
* 容差 1px:rect 是小数,点击坐标是整数,贴着边缘点也算命中。
|
|
294
|
+
*/
|
|
295
|
+
export function isPointOnHint(x, y, hint) {
|
|
296
|
+
return Array.from(hint.getClientRects()).some(function (rect) {
|
|
297
|
+
return x >= rect.left - 1 && x <= rect.right + 1 && y >= rect.top - 1 && y <= rect.bottom + 1;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 取点击坐标对应的插入点。Chrome / Safari 用 `caretRangeFromPoint`,Firefox 用 `caretPositionFromPoint`。 */
|
|
302
|
+
export function caretRangeFromPoint(x, y) {
|
|
303
|
+
var _doc$caretPositionFro;
|
|
304
|
+
var doc = document;
|
|
305
|
+
if (typeof doc.caretRangeFromPoint === 'function') {
|
|
306
|
+
return doc.caretRangeFromPoint(x, y);
|
|
307
|
+
}
|
|
308
|
+
var pos = (_doc$caretPositionFro = doc.caretPositionFromPoint) === null || _doc$caretPositionFro === void 0 ? void 0 : _doc$caretPositionFro.call(doc, x, y);
|
|
309
|
+
if (!pos) return null;
|
|
310
|
+
var range = document.createRange();
|
|
311
|
+
range.setStart(pos.offsetNode, pos.offset);
|
|
312
|
+
range.collapse(true);
|
|
313
|
+
return range;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* caret 是否已经「贴在 hint 左侧」——caret 到 hint 之间除了 chip / 光标锚点
|
|
318
|
+
* (ZWSP)之外没有别的内容。
|
|
319
|
+
*
|
|
320
|
+
* 用于拦截 ArrowRight:再往右一步就会跨到 hint 之后(哪怕只是一帧也会闪),
|
|
321
|
+
* 所以这里不能只认「紧邻 hint 的文本末尾」,要认「右侧已无正文」的所有位置。
|
|
322
|
+
*/
|
|
323
|
+
export function isCaretBeforeHint(range, editor) {
|
|
324
|
+
if (!range.collapsed) return false;
|
|
325
|
+
var hint = editor.querySelector(COMPOSER_HINT_SELECTOR);
|
|
326
|
+
if (!hint || !editor.contains(range.startContainer)) return false;
|
|
327
|
+
if (isPositionAfterHint(range.startContainer, range.startOffset, hint)) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
var between = document.createRange();
|
|
331
|
+
between.setStart(range.startContainer, range.startOffset);
|
|
332
|
+
between.setEndBefore(hint);
|
|
333
|
+
var hasVisibleText = stripInvisibleChars(between.toString()).replace(/\n/g, '') !== '';
|
|
334
|
+
if (hasVisibleText) return false;
|
|
335
|
+
return between.cloneContents().querySelector(CHIP_SELECTOR) === null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 派生 hint 之后不是合法插入点:hint 是只读派生文本,caret 停在它后面会让用户
|
|
340
|
+
* 误以为能在提示文案末尾继续输入(且视觉上离 chip 很远)。
|
|
341
|
+
*
|
|
342
|
+
* 命中时把 caret 拉回 hint 之前 —— 也就是「chip 右侧」这个唯一正确的落点。
|
|
343
|
+
* 覆盖点击末尾、End、ArrowRight / ArrowDown 落到行尾等所有路径。
|
|
344
|
+
*/
|
|
345
|
+
export function normalizeCollapsedCaretBeforeHint(range, editor) {
|
|
346
|
+
var _hint$parentElement;
|
|
347
|
+
if (!range.collapsed) return null;
|
|
348
|
+
if (!editor.contains(range.startContainer)) return null;
|
|
349
|
+
var hint = editor.querySelector(COMPOSER_HINT_SELECTOR);
|
|
350
|
+
var hintLine = (_hint$parentElement = hint === null || hint === void 0 ? void 0 : hint.parentElement) !== null && _hint$parentElement !== void 0 ? _hint$parentElement : null;
|
|
351
|
+
if (!hint || !hintLine) return null;
|
|
352
|
+
/* 只处理「hint 所在行」上的 caret。chip 之后允许存在空行(用户按 Enter
|
|
353
|
+
* 留了一行又把内容删光),那种情况下 caret 在别的行上,绝不能被拽回
|
|
354
|
+
* hint 这一行。 */
|
|
355
|
+
if (getCurrentLine(range.startContainer, editor) !== hintLine) return null;
|
|
356
|
+
if (!isPositionAfterHint(range.startContainer, range.startOffset, hint)) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
return getCaretBeforeHint(hint);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 查找 caret 所在行的「下一行」是否就是 hint 所在行 —— 用于拦截 ArrowDown:
|
|
364
|
+
* 该行除 chip + hint 外没有别的内容,唯二落点里只有 hint 之前合法,
|
|
365
|
+
* 所以直接落到那里,避免原生移动先跳到行尾(hint 之后)再被拉回的一帧闪烁。
|
|
366
|
+
*/
|
|
367
|
+
export function isHintOnNextLine(range, editor) {
|
|
368
|
+
var _hint$parentElement2;
|
|
369
|
+
var hint = editor.querySelector(COMPOSER_HINT_SELECTOR);
|
|
370
|
+
var hintLine = (_hint$parentElement2 = hint === null || hint === void 0 ? void 0 : hint.parentElement) !== null && _hint$parentElement2 !== void 0 ? _hint$parentElement2 : null;
|
|
371
|
+
if (!hint || !hintLine) return false;
|
|
372
|
+
var currentLine = getCurrentLine(range.startContainer, editor);
|
|
373
|
+
return !!currentLine && currentLine.nextElementSibling === hintLine;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** caret 是否已经在 hint 所在行上(End 会跳到该行行尾 = hint 之后)。 */
|
|
377
|
+
export function isCaretOnHintLine(range, editor) {
|
|
378
|
+
var _hint$parentElement3;
|
|
379
|
+
var hint = editor.querySelector(COMPOSER_HINT_SELECTOR);
|
|
380
|
+
var hintLine = (_hint$parentElement3 = hint === null || hint === void 0 ? void 0 : hint.parentElement) !== null && _hint$parentElement3 !== void 0 ? _hint$parentElement3 : null;
|
|
381
|
+
if (!hint || !hintLine) return false;
|
|
382
|
+
return getCurrentLine(range.startContainer, editor) === hintLine;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 上下方向键的落点是否会跑到 hint 上 / 之后。
|
|
387
|
+
*
|
|
388
|
+
* 两种形态都要覆盖:
|
|
389
|
+
* 1. hint 在相邻的 line div(↓ 时判定下一行,↑ 时判定上一行);
|
|
390
|
+
* 2. hint 与自己**同一个 line div 内软换行**(长文案折行,demo 的常态)→
|
|
391
|
+
* 按几何判定:hint 的任一视觉行落在「caret 上/下方一行」的区间里,
|
|
392
|
+
* 方向键就必然落进提示文案区域(那里没有可编辑落点,浏览器会给到 hint 之后)。
|
|
393
|
+
*
|
|
394
|
+
* 不用 `caretRangeFromPoint` 探测:hint 是 contentEditable=false,浏览器会把
|
|
395
|
+
* 该点解析成它前面最近的编辑位,探测结果反过来是「没越界」,靠不住。
|
|
396
|
+
*
|
|
397
|
+
* ↑ 只在 caret **不在** hint 所在行时才判定:同一条上按 ↑ 是正常上移。
|
|
398
|
+
*/
|
|
399
|
+
export function wouldArrowCrossHint(range, editor, direction) {
|
|
400
|
+
var _getCurrentLine$getBo, _getCurrentLine;
|
|
401
|
+
if (!range.collapsed) return false;
|
|
402
|
+
var hint = editor.querySelector(COMPOSER_HINT_SELECTOR);
|
|
403
|
+
if (!hint) return false;
|
|
404
|
+
if (direction === 'down' && isHintOnNextLine(range, editor)) return true;
|
|
405
|
+
if (direction === 'up' && isCaretOnHintLine(range, editor)) return false;
|
|
406
|
+
var hintRects = Array.from(hint.getClientRects());
|
|
407
|
+
if (hintRects.length === 0) return false;
|
|
408
|
+
|
|
409
|
+
/* 空行 / 元素偏移处的 collapsed range,getBoundingClientRect 可能全 0,
|
|
410
|
+
* 这时退化用所在行的盒子作为参照。 */
|
|
411
|
+
var rangeRect = range.getBoundingClientRect();
|
|
412
|
+
var lineRect = rangeRect.height ? null : (_getCurrentLine$getBo = (_getCurrentLine = getCurrentLine(range.startContainer, editor)) === null || _getCurrentLine === void 0 ? void 0 : _getCurrentLine.getBoundingClientRect()) !== null && _getCurrentLine$getBo !== void 0 ? _getCurrentLine$getBo : null;
|
|
413
|
+
var rect = rangeRect.height || !lineRect ? {
|
|
414
|
+
top: rangeRect.top,
|
|
415
|
+
bottom: rangeRect.bottom
|
|
416
|
+
} : {
|
|
417
|
+
top: lineRect.top,
|
|
418
|
+
bottom: lineRect.bottom
|
|
419
|
+
};
|
|
420
|
+
var lineHeight = rangeRect.height || (lineRect === null || lineRect === void 0 ? void 0 : lineRect.height) || 0 || parseFloat(getComputedStyle(editor).lineHeight) || 24;
|
|
421
|
+
if (direction === 'down') {
|
|
422
|
+
var _bandTop = rect.bottom;
|
|
423
|
+
var _bandBottom = rect.bottom + lineHeight;
|
|
424
|
+
return hintRects.some(function (r) {
|
|
425
|
+
return r.top <= _bandBottom + 1 && r.bottom > _bandTop + 1;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
var bandTop = rect.top - lineHeight;
|
|
429
|
+
var bandBottom = rect.top;
|
|
430
|
+
return hintRects.some(function (r) {
|
|
431
|
+
return r.top <= bandBottom && r.bottom >= bandTop + 1;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** 直接取当前 hint 元素的左侧落点(调用方负责写入 selection)。 */
|
|
436
|
+
export function caretBeforeHintIn(editor) {
|
|
437
|
+
var hint = editor.querySelector(COMPOSER_HINT_SELECTOR);
|
|
438
|
+
return hint ? getCaretBeforeHint(hint) : null;
|
|
439
|
+
}
|
|
234
440
|
export function resolveCaretMove(range, direction, editor) {
|
|
235
441
|
if (!editor.contains(range.startContainer)) return null;
|
|
236
442
|
if (direction === 'left') {
|
|
@@ -73,10 +73,11 @@ export interface ComposerEditorProps {
|
|
|
73
73
|
* 仅当 trigger 处于活动态(triggerRef.current 非空)时调用:
|
|
74
74
|
* 把 ↑↓ Enter Esc Tab 等导航键先丢给父组件让它驱动浮层选择。
|
|
75
75
|
* - 返回 `true` → editor 视为父已处理,不再走自身换行 / 提交逻辑
|
|
76
|
-
* - 返回 `false / undefined` → editor 走原默认(onPressEnter / 换行 / 删 chip 等)
|
|
76
|
+
* - 返回 `false / undefined` → editor 走原默认(onPressEnter / 换行 / 删 chip / Tab 采纳 hint 等)
|
|
77
77
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
78
|
+
* 带修饰键的 `Tab`(Shift+Tab 反向焦点、Ctrl/Cmd+Tab 切窗口)不转发,
|
|
79
|
+
* 保持浏览器默认。父组件如需 preventDefault,请在 handler 内自己调用——
|
|
80
|
+
* editor 不会替父代劳。
|
|
80
81
|
*/
|
|
81
82
|
onTriggerKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => boolean | void;
|
|
82
83
|
/**
|