one-design-next 0.0.113 → 0.0.115
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 +17 -3
- package/dist/composer/editor.js +423 -19
- package/dist/composer/hooks/useChipManager.d.ts +14 -2
- package/dist/composer/hooks/useChipManager.js +31 -16
- package/dist/composer/index.d.ts +40 -0
- package/dist/composer/index.js +72 -19
- package/dist/composer/inline-ref.d.ts +6 -1
- package/dist/composer/inline-ref.js +6 -1
- package/dist/composer/insert.d.ts +75 -0
- package/dist/composer/insert.js +130 -0
- 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 +30 -1
- package/dist/composer/utils.js +65 -3
- package/dist/icon/types.d.ts +1 -1
- package/dist/index.d.ts +3 -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/composer/editor.js
CHANGED
|
@@ -23,12 +23,13 @@ import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
|
|
|
23
23
|
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
|
|
24
24
|
import { createPortal } from 'react-dom';
|
|
25
25
|
import ScrollArea from "../scroll-area";
|
|
26
|
-
import { findChipHostBeforeCaret, mergeLineIntoPreviousAtCaretStart, normalizeCollapsedCaretAtChipBoundary, resolveCaretMove, splitLineAt } from "./caret";
|
|
26
|
+
import { caretBeforeHintIn, caretRangeFromPoint, findChipHostBeforeCaret, getCaretBeforeHint, getCurrentLine, isCaretBeforeHint, isCaretOnHintLine, isPointOnHint, isPositionAfterHint, mergeLineIntoPreviousAtCaretStart, normalizeCollapsedCaretAtChipBoundary, normalizeCollapsedCaretBeforeHint, resolveCaretMove, splitLineAt, wouldArrowCrossHint } from "./caret";
|
|
27
27
|
import { buildClipboardPayload, COMPOSER_CLIPBOARD_MIME, parseClipboardPayload, plainTextFromClipboardPayload, remapClipboardPayload } from "./clipboard";
|
|
28
28
|
import { useChipManager } from "./hooks/useChipManager";
|
|
29
29
|
import { useChipSelectionMarker } from "./hooks/useChipSelectionMarker";
|
|
30
30
|
import { chipHasEditableParams, ComposerInlineRef } from "./inline-ref";
|
|
31
|
-
import {
|
|
31
|
+
import { resolveHintTarget } from "./segments";
|
|
32
|
+
import { COMPOSER_HINT_ATTR, createLineRoot, createLineRootWithText, ensureCaretAnchorAfter, ensureLineRoots, getPlainText, isEditorEmpty, isLineRoot, LINE_ATTR_NAME, normalizeEditorDom, probeTrigger, stripInvisibleChars, ZWSP } from "./utils";
|
|
32
33
|
|
|
33
34
|
/** Composer editor 触发态对外信号——告诉父组件该弹浮层选 skill 了。 */
|
|
34
35
|
|
|
@@ -39,6 +40,11 @@ var sameProbe = function sameProbe(a, b) {
|
|
|
39
40
|
return a.textNode === b.textNode && a.triggerOffset === b.triggerOffset && a.caretOffset === b.caretOffset && a.trigger === b.trigger;
|
|
40
41
|
};
|
|
41
42
|
|
|
43
|
+
/** 归一化结果是否与当前 caret 完全一致(同节点同偏移)。 */
|
|
44
|
+
var isSameCaretPosition = function isSameCaretPosition(range, position) {
|
|
45
|
+
return range.startContainer === position.node && range.startOffset === position.offset;
|
|
46
|
+
};
|
|
47
|
+
|
|
42
48
|
/**
|
|
43
49
|
* 让 caret 始终保持在 viewport(ScrollArea 的可视区)内。
|
|
44
50
|
*
|
|
@@ -76,6 +82,109 @@ var scrollCaretIntoView = function scrollCaretIntoView(viewport) {
|
|
|
76
82
|
viewport.scrollTop -= viewportRect.top - rect.top + 4;
|
|
77
83
|
}
|
|
78
84
|
};
|
|
85
|
+
|
|
86
|
+
/** insertContent 的目标行:最后一个行容器;没有行模型时退回 editor 本身。 */
|
|
87
|
+
var resolveEndLine = function resolveEndLine(editor) {
|
|
88
|
+
var last = editor.lastElementChild;
|
|
89
|
+
return isLineRoot(last) ? last : editor;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** 行内直接子节点的派生 hint(不是正文,插入必须落在它之前)。 */
|
|
93
|
+
var findDirectHint = function findDirectHint(line) {
|
|
94
|
+
for (var _i = 0, _Array$from = Array.from(line.children); _i < _Array$from.length; _i++) {
|
|
95
|
+
var child = _Array$from[_i];
|
|
96
|
+
if (child.hasAttribute(COMPOSER_HINT_ATTR)) return child;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* insertContent 的插入点:最后一个行容器的内容末尾。
|
|
103
|
+
*
|
|
104
|
+
* 不能直接 `selectNodeContents(editor).collapse(false)`:
|
|
105
|
+
* - 行尾的派生 hint 不是内容:落在 hint 之前,避免内容插到灰字后面
|
|
106
|
+
* - 行尾的占位 `<br>` 不是换行:插到它前面。插到后面会让它从「占位」变成
|
|
107
|
+
* 真实换行,`getPlainText` 就会凭空多输出一个 '\n'
|
|
108
|
+
*/
|
|
109
|
+
var resolveEndRange = function resolveEndRange(editor) {
|
|
110
|
+
var line = resolveEndLine(editor);
|
|
111
|
+
var range = document.createRange();
|
|
112
|
+
var hint = findDirectHint(line);
|
|
113
|
+
if (hint) {
|
|
114
|
+
var before = getCaretBeforeHint(hint);
|
|
115
|
+
if (before) {
|
|
116
|
+
range.setStart(before.node, before.offset);
|
|
117
|
+
range.collapse(true);
|
|
118
|
+
return range;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
var offset = line.childNodes.length;
|
|
122
|
+
while (offset > 0) {
|
|
123
|
+
var node = line.childNodes[offset - 1];
|
|
124
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') {
|
|
125
|
+
offset -= 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
range.setStart(line, offset);
|
|
131
|
+
range.collapse(true);
|
|
132
|
+
return range;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* insertContent 之后把 caret 落到新内容末尾。
|
|
137
|
+
*
|
|
138
|
+
* 行尾是 chip 时补一颗不进 value 的 ZWSP 锚点(与 insertChip 一致):
|
|
139
|
+
* 只靠元素偏移落位时,部分浏览器会把 caret 带到结构块之后。
|
|
140
|
+
* 行尾有派生 hint 时仍落在 hint 之前(hint 之后不是合法插入点)。
|
|
141
|
+
*/
|
|
142
|
+
var placeCaretAtEnd = function placeCaretAtEnd(editor) {
|
|
143
|
+
var sel = window.getSelection();
|
|
144
|
+
if (!sel) return;
|
|
145
|
+
var line = resolveEndLine(editor);
|
|
146
|
+
var target;
|
|
147
|
+
var hint = findDirectHint(line);
|
|
148
|
+
var beforeHint = hint ? getCaretBeforeHint(hint) : null;
|
|
149
|
+
if (beforeHint) {
|
|
150
|
+
target = beforeHint;
|
|
151
|
+
} else {
|
|
152
|
+
var offset = line.childNodes.length;
|
|
153
|
+
while (offset > 0) {
|
|
154
|
+
var node = line.childNodes[offset - 1];
|
|
155
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') {
|
|
156
|
+
offset -= 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
var tail = offset > 0 ? line.childNodes[offset - 1] : null;
|
|
162
|
+
if ((tail === null || tail === void 0 ? void 0 : tail.nodeType) === Node.TEXT_NODE) {
|
|
163
|
+
var _tail$textContent;
|
|
164
|
+
target = {
|
|
165
|
+
node: tail,
|
|
166
|
+
offset: ((_tail$textContent = tail.textContent) !== null && _tail$textContent !== void 0 ? _tail$textContent : '').length
|
|
167
|
+
};
|
|
168
|
+
} else if (tail instanceof HTMLElement && tail.hasAttribute('data-odn-composer-chip-host')) {
|
|
169
|
+
var _anchor$textContent;
|
|
170
|
+
var _anchor = ensureCaretAnchorAfter(tail);
|
|
171
|
+
target = {
|
|
172
|
+
node: _anchor,
|
|
173
|
+
offset: ((_anchor$textContent = _anchor.textContent) !== null && _anchor$textContent !== void 0 ? _anchor$textContent : '').length
|
|
174
|
+
};
|
|
175
|
+
} else {
|
|
176
|
+
target = {
|
|
177
|
+
node: line,
|
|
178
|
+
offset: offset
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
var range = document.createRange();
|
|
183
|
+
range.setStart(target.node, target.offset);
|
|
184
|
+
range.collapse(true);
|
|
185
|
+
sel.removeAllRanges();
|
|
186
|
+
sel.addRange(range);
|
|
187
|
+
};
|
|
79
188
|
var LINE_HEIGHT = 24;
|
|
80
189
|
/** 与 SCSS `padding: 2px … 0` 对齐。10 行 = 10 * 24 + 2 = 242px。 */
|
|
81
190
|
var EDITOR_PADDING_BLOCK = 2;
|
|
@@ -237,11 +346,124 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
237
346
|
}
|
|
238
347
|
};
|
|
239
348
|
}, []);
|
|
349
|
+
|
|
350
|
+
/* chips 快照读取器:fireChange 声明在 useChipManager 之前(后者又依赖 fireChange
|
|
351
|
+
* 当 onAfterMutate),用 ref 打破声明顺序,与 recomputeTriggerRef 同一套做法。 */
|
|
352
|
+
var getChipsRef = useRef(function () {
|
|
353
|
+
return [];
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
/* ---- 派生 hint:imperative DOM 同步 ----
|
|
357
|
+
* hint 必须是 chip host 的**兄弟节点**,不能塞在 host 内部:host 是
|
|
358
|
+
* contentEditable=false 的原子节点,塞进去后「chip 之后、hint 之前」这个位置
|
|
359
|
+
* 就不存在了,caret 只能落在整个 host 之后(= 提示文本末尾)。
|
|
360
|
+
*
|
|
361
|
+
* 它不归 React 管(editor 的 children 由 contenteditable 自治),由 fireChange
|
|
362
|
+
* 按结构谓词增删/挪位;`getPlainText` 会整棵子树跳过,所以不进 value / 剪贴板。 */
|
|
363
|
+
var hintNodeRef = useRef(null);
|
|
364
|
+
var removeHintNode = useCallback(function () {
|
|
365
|
+
var _hintNodeRef$current;
|
|
366
|
+
(_hintNodeRef$current = hintNodeRef.current) === null || _hintNodeRef$current === void 0 || _hintNodeRef$current.remove();
|
|
367
|
+
hintNodeRef.current = null;
|
|
368
|
+
}, []);
|
|
369
|
+
var syncHintNode = useCallback(function (target) {
|
|
370
|
+
var _Array$from$find, _blankTail;
|
|
371
|
+
var el = editorRef.current;
|
|
372
|
+
var host = el && target ? (_Array$from$find = Array.from(el.querySelectorAll('[data-mention-id]')).find(function (node) {
|
|
373
|
+
return node.dataset.mentionId === target.chipId;
|
|
374
|
+
})) !== null && _Array$from$find !== void 0 ? _Array$from$find : null : null;
|
|
375
|
+
if (!target || !host || !host.parentNode) {
|
|
376
|
+
removeHintNode();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
var node = hintNodeRef.current;
|
|
380
|
+
if (!node || !node.isConnected) {
|
|
381
|
+
node = document.createElement('span');
|
|
382
|
+
node.setAttribute(COMPOSER_HINT_ATTR, '');
|
|
383
|
+
node.contentEditable = 'false';
|
|
384
|
+
hintNodeRef.current = node;
|
|
385
|
+
}
|
|
386
|
+
if (node.textContent !== target.text) node.textContent = target.text;
|
|
387
|
+
|
|
388
|
+
/* 落位:命中 chip 之后、**紧随其后的不可见锚点文本(ZWSP)之后**。
|
|
389
|
+
*
|
|
390
|
+
* 谓词要求 chip 之后没有任何内容,所以这里能走过去的只有不进 value 的
|
|
391
|
+
* 锚点文本(caret.ts / insertChip 在 chip 与 hint 之间塞的 ZWSP)。hint
|
|
392
|
+
* 若插在锚点前面,caret 就只剩「元素偏移」一个落点,每次输入后还会被
|
|
393
|
+
* 拽回去重排;排在锚点之后,caret 落在真实的文本节点里,位置稳定。
|
|
394
|
+
* 注意只跳过「纯不可见字符」的文本:空格 / 换行是内容(谓词会让 hint
|
|
395
|
+
* 直接消失),不能把 hint 推到它们后面。 */
|
|
396
|
+
var parent = host.parentNode;
|
|
397
|
+
var blankTail = null;
|
|
398
|
+
var cur = host.nextSibling;
|
|
399
|
+
while (cur) {
|
|
400
|
+
var _cur$textContent;
|
|
401
|
+
// 跳过 hint 自身:上一轮若把它排到了锚点前面,这里顺手挪回末尾
|
|
402
|
+
if (cur === node) {
|
|
403
|
+
cur = cur.nextSibling;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (cur.nodeType === Node.TEXT_NODE && stripInvisibleChars((_cur$textContent = cur.textContent) !== null && _cur$textContent !== void 0 ? _cur$textContent : '') === '') {
|
|
407
|
+
blankTail = cur;
|
|
408
|
+
cur = cur.nextSibling;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
/* 没有现成锚点时必须补一个:草稿还原(defaultValue + defaultChips)走的
|
|
414
|
+
* 是 insertSerializedAtRange,不会像 insertChip 那样在 chip 后塞 ZWSP,
|
|
415
|
+
* 于是 chip 与 hint 之间没有任何可编辑文本节点。此时用户第一次点击 hint,
|
|
416
|
+
* 原生 caret 无处可落,Chrome 会先把它放进 hint 内部(首帧画在提示文案
|
|
417
|
+
* 开头),下一拍才被 selectionchange 拉回来 —— 也就是「进页面第一次点
|
|
418
|
+
* hint,光标先闪一下」。垫上锚点后原生落点直接命中它。
|
|
419
|
+
* 锚点不进 value(stripInvisibleChars / getPlainText 都会剔除)。 */
|
|
420
|
+
if (!blankTail) {
|
|
421
|
+
var caretAnchor = document.createTextNode(ZWSP);
|
|
422
|
+
parent.insertBefore(caretAnchor, host.nextSibling);
|
|
423
|
+
blankTail = caretAnchor;
|
|
424
|
+
}
|
|
425
|
+
var anchor = (_blankTail = blankTail) !== null && _blankTail !== void 0 ? _blankTail : host;
|
|
426
|
+
if (node.parentNode !== parent || node.previousSibling !== anchor) {
|
|
427
|
+
parent.insertBefore(node, anchor.nextSibling);
|
|
428
|
+
}
|
|
429
|
+
}, [removeHintNode]);
|
|
430
|
+
|
|
431
|
+
/** 把 caret 直接落到 hint 之前(= chip 右侧);没有 hint 时不动。 */
|
|
432
|
+
var placeCaretBeforeHint = useCallback(function () {
|
|
433
|
+
var el = editorRef.current;
|
|
434
|
+
var target = el ? caretBeforeHintIn(el) : null;
|
|
435
|
+
var sel = window.getSelection();
|
|
436
|
+
if (!target || !sel) return false;
|
|
437
|
+
var range = document.createRange();
|
|
438
|
+
range.setStart(target.node, target.offset);
|
|
439
|
+
range.collapse(true);
|
|
440
|
+
sel.removeAllRanges();
|
|
441
|
+
sel.addRange(range);
|
|
442
|
+
lastEditorRangeRef.current = range.cloneRange();
|
|
443
|
+
return true;
|
|
444
|
+
}, []);
|
|
445
|
+
|
|
446
|
+
/** hint 之后不是合法插入点:把跑过去的 caret 拉回 hint 之前(chip 右侧)。 */
|
|
447
|
+
var pullCaretBeforeHint = useCallback(function () {
|
|
448
|
+
var el = editorRef.current;
|
|
449
|
+
var sel = window.getSelection();
|
|
450
|
+
if (!el || !sel || sel.rangeCount !== 1 || !sel.isCollapsed) return;
|
|
451
|
+
if (!el.contains(sel.anchorNode)) return;
|
|
452
|
+
if (!normalizeCollapsedCaretBeforeHint(sel.getRangeAt(0), el)) return;
|
|
453
|
+
placeCaretBeforeHint();
|
|
454
|
+
}, [placeCaretBeforeHint]);
|
|
240
455
|
var fireChange = useCallback(function () {
|
|
241
456
|
var el = editorRef.current;
|
|
242
457
|
if (!el) return;
|
|
243
458
|
// normalizeEditorDom 内部已 ensureLineRoots(commit-2),此处不必重复兜底
|
|
244
459
|
normalizeEditorDom(el);
|
|
460
|
+
var chips = getChipsRef.current();
|
|
461
|
+
var raw = getPlainText(el);
|
|
462
|
+
/* 派生 hint 与 value 同源同刻重算:结构谓词只依赖 raw + chips(换行也算
|
|
463
|
+
* 内容,所以按 Enter 后 hint 自动消失)。必须先落 DOM 再算 empty / 高度——
|
|
464
|
+
* 否则「chip 被删」时残留的 hint 文本会让 placeholder 判定与 lite 升格
|
|
465
|
+
* 判定晚一拍。 */
|
|
466
|
+
syncHintNode(resolveHintTarget(raw, chips));
|
|
245
467
|
var showPlaceholder = shouldShowPlaceholder(el);
|
|
246
468
|
if (forceNonEmptyOnceRef.current && showPlaceholder) {
|
|
247
469
|
setEmpty(false);
|
|
@@ -249,11 +471,12 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
249
471
|
setEmpty(showPlaceholder);
|
|
250
472
|
}
|
|
251
473
|
forceNonEmptyOnceRef.current = false;
|
|
252
|
-
onChange === null || onChange === void 0 || onChange(
|
|
474
|
+
onChange === null || onChange === void 0 || onChange(raw);
|
|
475
|
+
pullCaretBeforeHint();
|
|
253
476
|
scheduleControlledSync();
|
|
254
477
|
// editor.height 锁定时 RO 不触发,主动同步一次,确保升格判定能拿到 scrollHeight
|
|
255
478
|
syncHeight();
|
|
256
|
-
}, [onChange, scheduleControlledSync, shouldShowPlaceholder, syncHeight]);
|
|
479
|
+
}, [onChange, pullCaretBeforeHint, scheduleControlledSync, shouldShowPlaceholder, syncHeight, syncHintNode]);
|
|
257
480
|
|
|
258
481
|
/* ─────────────────────────────────────
|
|
259
482
|
* chip 管理
|
|
@@ -272,6 +495,7 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
272
495
|
getChipHost = _useChipManager.getChipHost,
|
|
273
496
|
getChips = _useChipManager.getChips,
|
|
274
497
|
resetChips = _useChipManager.resetChips;
|
|
498
|
+
getChipsRef.current = getChips;
|
|
275
499
|
useChipSelectionMarker(editorRef, chips);
|
|
276
500
|
var insertSerializedAtRangeRef = useRef(insertSerializedAtRange);
|
|
277
501
|
insertSerializedAtRangeRef.current = insertSerializedAtRange;
|
|
@@ -305,6 +529,50 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
305
529
|
}, [triggers, onTriggerChange, probeToInfo]);
|
|
306
530
|
recomputeTriggerRef.current = recomputeTrigger;
|
|
307
531
|
|
|
532
|
+
/* ─────────────────────────────────────
|
|
533
|
+
* Tab 采纳 hint
|
|
534
|
+
*
|
|
535
|
+
* hint 只是结构投影(不进 value);caret 恰好停在 chip 与 hint 之间
|
|
536
|
+
* (= 内容末尾)时按 Tab,表示用户接受这段模板,于是把提示文案落成正文:
|
|
537
|
+
* chip 之后出现内容 → 结构谓词不再命中 → 灰字自动移除,onChange 抛出的
|
|
538
|
+
* value 里带上这段文案,与用户自己敲进去的效果一致。
|
|
539
|
+
* ───────────────────────────────────── */
|
|
540
|
+
var acceptHintAsText = useCallback(function () {
|
|
541
|
+
var _hint$textContent;
|
|
542
|
+
var el = editorRef.current;
|
|
543
|
+
var hint = hintNodeRef.current;
|
|
544
|
+
var sel = window.getSelection();
|
|
545
|
+
if (!el || !hint || !hint.isConnected || !sel || sel.rangeCount === 0) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
var range = sel.getRangeAt(0);
|
|
549
|
+
if (!range.collapsed || !el.contains(range.startContainer)) return false;
|
|
550
|
+
/* 只认「chip 之后、hint 之前」这一个落点;caret 在别处时 Tab 仍归浏览器,
|
|
551
|
+
* 避免在任意位置误吞焦点切换。 */
|
|
552
|
+
if (!isCaretBeforeHint(range, el)) return false;
|
|
553
|
+
var text = (_hint$textContent = hint.textContent) !== null && _hint$textContent !== void 0 ? _hint$textContent : '';
|
|
554
|
+
if (!text) return false;
|
|
555
|
+
|
|
556
|
+
/* 多行模板按行模型展开(与粘贴纯文本同一路径);单行直接插文本节点。 */
|
|
557
|
+
if (text.includes('\n')) {
|
|
558
|
+
insertSerializedAtRange(text, [], range, el);
|
|
559
|
+
} else {
|
|
560
|
+
var node = document.createTextNode(text);
|
|
561
|
+
range.insertNode(node);
|
|
562
|
+
var caret = document.createRange();
|
|
563
|
+
/* 落点写进文本节点内部(而不是 setStartAfter 的元素偏移),后续输入
|
|
564
|
+
* 直接在同一节点里续写,caret 也更稳定。 */
|
|
565
|
+
caret.setStart(node, text.length);
|
|
566
|
+
caret.collapse(true);
|
|
567
|
+
sel.removeAllRanges();
|
|
568
|
+
sel.addRange(caret);
|
|
569
|
+
}
|
|
570
|
+
fireChange();
|
|
571
|
+
recomputeTrigger();
|
|
572
|
+
requestScrollCaretIntoView();
|
|
573
|
+
return true;
|
|
574
|
+
}, [fireChange, insertSerializedAtRange, recomputeTrigger, requestScrollCaretIntoView]);
|
|
575
|
+
|
|
308
576
|
/* ─────────────────────────────────────
|
|
309
577
|
* ref 命令式 API
|
|
310
578
|
* ───────────────────────────────────── */
|
|
@@ -323,6 +591,7 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
323
591
|
if (!el) return;
|
|
324
592
|
el.innerHTML = '';
|
|
325
593
|
el.appendChild(createLineRoot());
|
|
594
|
+
removeHintNode();
|
|
326
595
|
resetChips();
|
|
327
596
|
setEmpty(true);
|
|
328
597
|
if (triggerRef.current) {
|
|
@@ -370,6 +639,33 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
370
639
|
onTriggerChange === null || onTriggerChange === void 0 || onTriggerChange(null);
|
|
371
640
|
_insertChip(data, el);
|
|
372
641
|
},
|
|
642
|
+
insertSerializedAtEnd: function insertSerializedAtEnd(value, nextChips, options) {
|
|
643
|
+
var el = editorRef.current;
|
|
644
|
+
if (!el || !value) return;
|
|
645
|
+
ensureLineRoots(el);
|
|
646
|
+
var range = resolveEndRange(el);
|
|
647
|
+
var shouldFocus = (options === null || options === void 0 ? void 0 : options.focus) !== false;
|
|
648
|
+
/* 拿不到 Selection 时仍要把内容插进去,只是不主动落 caret。 */
|
|
649
|
+
var sel = shouldFocus ? window.getSelection() : null;
|
|
650
|
+
if (sel) {
|
|
651
|
+
sel.removeAllRanges();
|
|
652
|
+
sel.addRange(range);
|
|
653
|
+
}
|
|
654
|
+
insertSerializedAtRange(value, nextChips, range, el, {
|
|
655
|
+
focus: shouldFocus,
|
|
656
|
+
caret: !!sel
|
|
657
|
+
});
|
|
658
|
+
if (!shouldFocus) return;
|
|
659
|
+
placeCaretAtEnd(el);
|
|
660
|
+
if (sel && sel.rangeCount > 0) {
|
|
661
|
+
lastEditorRangeRef.current = sel.getRangeAt(0).cloneRange();
|
|
662
|
+
}
|
|
663
|
+
requestScrollCaretIntoView();
|
|
664
|
+
},
|
|
665
|
+
getValue: function getValue() {
|
|
666
|
+
var el = editorRef.current;
|
|
667
|
+
return el ? getPlainText(el) : '';
|
|
668
|
+
},
|
|
373
669
|
cancelTrigger: function cancelTrigger() {
|
|
374
670
|
if (!triggerRef.current) return;
|
|
375
671
|
triggerRef.current = null;
|
|
@@ -392,7 +688,7 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
392
688
|
return editorRef.current;
|
|
393
689
|
}
|
|
394
690
|
};
|
|
395
|
-
}, [onChange, _insertChip, removeChip, resetChips, onTriggerChange, getChips, updateChipData, getChipHost]);
|
|
691
|
+
}, [onChange, _insertChip, insertSerializedAtRange, requestScrollCaretIntoView, removeChip, resetChips, onTriggerChange, getChips, updateChipData, getChipHost]);
|
|
396
692
|
|
|
397
693
|
/* ─────────────────────────────────────
|
|
398
694
|
* 监听 selection 变化:方向键 / 鼠标点击移位都要重算 trigger
|
|
@@ -418,8 +714,15 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
418
714
|
}
|
|
419
715
|
var range = sel.getRangeAt(0);
|
|
420
716
|
if (range.collapsed) {
|
|
421
|
-
var
|
|
422
|
-
|
|
717
|
+
var _normalizeCollapsedCa;
|
|
718
|
+
/* hint 之后不是合法插入点(点击行尾 / End / 方向键跑到提示文案后面),
|
|
719
|
+
* 优先拉回 hint 左侧;其余 chip 边界仍走原规范化。 */
|
|
720
|
+
var normalized = (_normalizeCollapsedCa = normalizeCollapsedCaretBeforeHint(range, el)) !== null && _normalizeCollapsedCa !== void 0 ? _normalizeCollapsedCa : normalizeCollapsedCaretAtChipBoundary(range, el);
|
|
721
|
+
/* 归一化到「原地」时不能再回写:removeAllRanges + addRange 会再次派发
|
|
722
|
+
* selectionchange,形成「归一化 → 回写 → 再归一化」的死循环。
|
|
723
|
+
* chip 后只剩 ZWSP 锚点(hint 场景的常态)时归一化结果必然等于当前
|
|
724
|
+
* caret,正是这条循环最容易命中的地方。 */
|
|
725
|
+
if (normalized && !isSameCaretPosition(range, normalized)) {
|
|
423
726
|
var r = document.createRange();
|
|
424
727
|
r.setStart(normalized.node, normalized.offset);
|
|
425
728
|
r.collapse(true);
|
|
@@ -458,7 +761,12 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
458
761
|
var range = document.createRange();
|
|
459
762
|
range.selectNodeContents(firstLine);
|
|
460
763
|
range.collapse(true);
|
|
461
|
-
|
|
764
|
+
/* 首屏还原不是用户操作:不要在这里 focus。否则挂载时浏览器会把
|
|
765
|
+
* 页面滚动到该编辑器(多个 demo / 多处草稿还原时,最后聚焦的那个
|
|
766
|
+
* 会决定滚动落点),用户看到的就是「一进页面就跳位置」。 */
|
|
767
|
+
insertSerializedAtRangeRef.current(initial, initialChips, range, el, {
|
|
768
|
+
focus: false
|
|
769
|
+
});
|
|
462
770
|
}
|
|
463
771
|
} else {
|
|
464
772
|
var lines = (initial !== null && initial !== void 0 ? initial : '').split('\n');
|
|
@@ -528,11 +836,13 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
528
836
|
var line = _step2.value;
|
|
529
837
|
el.appendChild(createLineRootWithText(line));
|
|
530
838
|
}
|
|
839
|
+
// 重建后没有任何 chip,hint 一并清掉(外部 value 只按纯文本还原)
|
|
531
840
|
} catch (err) {
|
|
532
841
|
_iterator2.e(err);
|
|
533
842
|
} finally {
|
|
534
843
|
_iterator2.f();
|
|
535
844
|
}
|
|
845
|
+
removeHintNode();
|
|
536
846
|
resetChips();
|
|
537
847
|
setEmpty(shouldShowPlaceholder(el));
|
|
538
848
|
if (triggerRef.current) {
|
|
@@ -569,7 +879,10 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
569
879
|
isComposingRef.current = true;
|
|
570
880
|
// 合成期一定会有候选字符出现,立刻收 placeholder(输入事件会进一步刷新)
|
|
571
881
|
setEmpty(false);
|
|
572
|
-
|
|
882
|
+
/* 合成期 hint 先摘掉:候选串会插在 chip/hint 之间,留着灰字会和候选文字挤在
|
|
883
|
+
* 一起且语义已经失效(用户开始自己写内容了)。compositionEnd 会按谓词重算。 */
|
|
884
|
+
removeHintNode();
|
|
885
|
+
}, [removeHintNode]);
|
|
573
886
|
var handleCompositionEnd = useCallback(function () {
|
|
574
887
|
isComposingRef.current = false;
|
|
575
888
|
var el = editorRef.current;
|
|
@@ -620,10 +933,19 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
620
933
|
/* 触发态优先:activeTrigger 时把导航键先吐给父组件(驱动浮层选择)。
|
|
621
934
|
* 父返回 true 表示已处理,editor 直接结束本次 keydown,不再走任何
|
|
622
935
|
* 自身逻辑(避免 Enter 又触发 onPressEnter / 插换行)。 */
|
|
623
|
-
if (triggerRef.current && onTriggerKeyDown && (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' && !e.shiftKey || e.key === 'Escape' || e.key === 'Tab')) {
|
|
936
|
+
if (triggerRef.current && onTriggerKeyDown && (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' && !e.shiftKey || e.key === 'Escape' || e.key === 'Tab' && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey)) {
|
|
624
937
|
if (onTriggerKeyDown(e) === true) return;
|
|
625
938
|
}
|
|
626
939
|
|
|
940
|
+
/* Tab 采纳 hint:caret 停在 chip 与派生 hint 之间(内容末尾)时,把提示
|
|
941
|
+
* 文案落成正文。浮层展开时 Tab 已被父组件优先用于选中技能(上面已 return);
|
|
942
|
+
* 走到这里说明浮层没开 / 没有命中,才轮到 hint 采纳。带修饰键的
|
|
943
|
+
* Tab(Shift+Tab 反向焦点、Ctrl/Cmd+Tab 切窗口)不拦,保持浏览器默认。 */
|
|
944
|
+
if (e.key === 'Tab' && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && acceptHintAsText()) {
|
|
945
|
+
e.preventDefault();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
|
|
627
949
|
// Backspace:chip 边界——空 text 在 chip 后先退回 chip 前;chip 前无正文再整块删
|
|
628
950
|
if (e.key === 'Backspace') {
|
|
629
951
|
var el = editorRef.current;
|
|
@@ -651,14 +973,39 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
651
973
|
}
|
|
652
974
|
}
|
|
653
975
|
|
|
976
|
+
/* hint 之后不是合法插入点,而 ArrowRight / ArrowDown / End 原生的下一跳都会先跨
|
|
977
|
+
* 过去、再由 selectionchange 拉回——视觉上闪一帧。这里在按键阶段直接拦下,
|
|
978
|
+
* 把 caret 落到 hint 之前(那时它就是唯一合法的落点)。 */
|
|
979
|
+
if (!e.shiftKey && (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'End')) {
|
|
980
|
+
var _sel = window.getSelection();
|
|
981
|
+
var _el = editorRef.current;
|
|
982
|
+
var _range = _sel && _sel.rangeCount > 0 ? _sel.getRangeAt(0) : null;
|
|
983
|
+
if (_sel && _el && _range && _range.collapsed && _el.contains(_range.startContainer)) {
|
|
984
|
+
/* Ctrl/Cmd+End 会跳到整篇末尾(= hint 之后),与普通 End 同样拦下 */
|
|
985
|
+
var isDocEnd = e.key === 'End' && (e.ctrlKey || e.metaKey);
|
|
986
|
+
var isVertical = e.key === 'ArrowDown' || e.key === 'ArrowUp';
|
|
987
|
+
var crossesHint = e.key === 'ArrowRight' && isCaretBeforeHint(_range, _el) || isVertical && wouldArrowCrossHint(_range, _el, e.key === 'ArrowDown' ? 'down' : 'up') || e.key === 'End' && (isDocEnd || isCaretOnHintLine(_range, _el));
|
|
988
|
+
if (crossesHint) {
|
|
989
|
+
e.preventDefault();
|
|
990
|
+
/* ↓ 若本来就在 hint 所在行,则「无处可去」→ 原地不动;
|
|
991
|
+
* 其余情况统一落到 hint 之前(那里才是唯一合法插入点)。 */
|
|
992
|
+
if (!(e.key === 'ArrowDown' && isCaretOnHintLine(_range, _el))) {
|
|
993
|
+
placeCaretBeforeHint();
|
|
994
|
+
}
|
|
995
|
+
recomputeTrigger();
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
654
1001
|
// ArrowLeft / ArrowRight:跨越 chip 边界时一次到位(绕开 ZWSP 锚点造成的"卡一格")
|
|
655
1002
|
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && !e.shiftKey) {
|
|
656
|
-
var
|
|
657
|
-
if (
|
|
1003
|
+
var _sel2 = window.getSelection();
|
|
1004
|
+
if (_sel2 && _sel2.rangeCount > 0) {
|
|
658
1005
|
var _editorRef$current3;
|
|
659
|
-
var
|
|
660
|
-
if (
|
|
661
|
-
var target = resolveCaretMove(
|
|
1006
|
+
var _range2 = _sel2.getRangeAt(0);
|
|
1007
|
+
if (_range2.collapsed && (_editorRef$current3 = editorRef.current) !== null && _editorRef$current3 !== void 0 && _editorRef$current3.contains(_range2.startContainer)) {
|
|
1008
|
+
var target = resolveCaretMove(_range2, e.key === 'ArrowLeft' ? 'left' : 'right', editorRef.current);
|
|
662
1009
|
if (target) {
|
|
663
1010
|
e.preventDefault();
|
|
664
1011
|
var r = document.createRange();
|
|
@@ -666,8 +1013,8 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
666
1013
|
r.collapse(true);
|
|
667
1014
|
// 本次已在 keydown 同步完成 caret 落位:跳过下一次 selectionchange 重算。
|
|
668
1015
|
suppressSelectionChangeOnceRef.current = true;
|
|
669
|
-
|
|
670
|
-
|
|
1016
|
+
_sel2.removeAllRanges();
|
|
1017
|
+
_sel2.addRange(r);
|
|
671
1018
|
lastEditorRangeRef.current = r.cloneRange();
|
|
672
1019
|
recomputeTrigger();
|
|
673
1020
|
return;
|
|
@@ -685,10 +1032,66 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
685
1032
|
e.preventDefault();
|
|
686
1033
|
insertLineBreak();
|
|
687
1034
|
}
|
|
688
|
-
}, [inputModality, onPressEnter, insertLineBreak, removeChip, onTriggerKeyDown, fireChange, recomputeTrigger]);
|
|
1035
|
+
}, [inputModality, onPressEnter, insertLineBreak, placeCaretBeforeHint, removeChip, onTriggerKeyDown, acceptHintAsText, fireChange, recomputeTrigger]);
|
|
689
1036
|
var handlePointerInput = useCallback(function () {
|
|
690
1037
|
if (inputModality !== 'pointer') setInputModality('pointer');
|
|
691
1038
|
}, [inputModality]);
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* mouseup 兜底:极少数路径(多击 / 浏览器差异)caret 可能在 mousedown 之后才
|
|
1042
|
+
* 落位。这里再校一次,仍然在下一帧绘制之前完成,不会留下闪帧。
|
|
1043
|
+
*/
|
|
1044
|
+
var handleEditorMouseUp = useCallback(function () {
|
|
1045
|
+
pullCaretBeforeHint();
|
|
1046
|
+
}, [pullCaretBeforeHint]);
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* 点击落在 hint 之后(含点在提示文案自己身上)时,浏览器会先把 caret 放到
|
|
1050
|
+
* 提示文案末尾,下一帧才被 selectionchange 拉回来 —— 视觉上就是「闪一下」。
|
|
1051
|
+
* 这里用点击坐标预判原生落点,命中就 preventDefault 并自己落 caret,
|
|
1052
|
+
* 连那一帧都不给。
|
|
1053
|
+
*
|
|
1054
|
+
* 注意不要按 `e.detail` 放行多击:连点(双击 / 三击)时 `detail` 会变成 2、3,
|
|
1055
|
+
* 一旦放行,第二次点击就又会先落到 hint 之后(高频点击复现的原因)。
|
|
1056
|
+
* 该区域本来就没有可选文本(hint 是 user-select:none 的派生展示),
|
|
1057
|
+
* 双击选词/三击整行在这里没有语义,拦掉是安全的。
|
|
1058
|
+
*/
|
|
1059
|
+
var handleEditorMouseDown = useCallback(function (e) {
|
|
1060
|
+
handlePointerInput();
|
|
1061
|
+
if (e.button !== 0 || disabled) return;
|
|
1062
|
+
// shift+click 是扩选,不劫持
|
|
1063
|
+
if (e.shiftKey) return;
|
|
1064
|
+
var el = editorRef.current;
|
|
1065
|
+
var hint = hintNodeRef.current;
|
|
1066
|
+
if (!el || !hint || !hint.isConnected) return;
|
|
1067
|
+
|
|
1068
|
+
/* 两条命中判据:
|
|
1069
|
+
* 1. 坐标直接落在 hint 的视觉行上 —— 原生落点不可控(可能进 hint 内部、
|
|
1070
|
+
* 行首、甚至 hint 之后),只有坐标是确定的;
|
|
1071
|
+
* 2. 原生落点解析在 hint 之后(点在同一行右侧留白等处)。
|
|
1072
|
+
* 命中任一条都接管,别让原生 caret 先画一帧。 */
|
|
1073
|
+
var onHint = isPointOnHint(e.clientX, e.clientY, hint);
|
|
1074
|
+
if (!onHint) {
|
|
1075
|
+
var point = caretRangeFromPoint(e.clientX, e.clientY);
|
|
1076
|
+
if (!point) return;
|
|
1077
|
+
if (!isPositionAfterHint(point.startContainer, point.startOffset, hint)) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
/* chip 之后可能有空行(用户按 Enter 留了一行又把内容删光):点在那
|
|
1081
|
+
* 些行上要正常落 caret,不能被拽回 hint 所在行。pointLine 为空 =
|
|
1082
|
+
* 点在行留白 / 编辑器 padding 上,这种仍按「末尾」处理。 */
|
|
1083
|
+
var pointLine = getCurrentLine(point.startContainer, el);
|
|
1084
|
+
if (pointLine && pointLine !== hint.parentElement) return;
|
|
1085
|
+
}
|
|
1086
|
+
e.preventDefault();
|
|
1087
|
+
/* 先 focus 再写 selection:未聚焦时写进去的 range 会被 focus 的默认行为
|
|
1088
|
+
* 覆盖(首次点击恰好是这种情况)。 */
|
|
1089
|
+
el.focus({
|
|
1090
|
+
preventScroll: true
|
|
1091
|
+
});
|
|
1092
|
+
placeCaretBeforeHint();
|
|
1093
|
+
requestScrollCaretIntoView();
|
|
1094
|
+
}, [disabled, handlePointerInput, placeCaretBeforeHint, requestScrollCaretIntoView]);
|
|
692
1095
|
var writeClipboardPayload = useCallback(function (e, payload) {
|
|
693
1096
|
if (!payload) return false;
|
|
694
1097
|
e.clipboardData.setData(COMPOSER_CLIPBOARD_MIME, JSON.stringify(payload));
|
|
@@ -817,7 +1220,8 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
|
|
|
817
1220
|
onCompositionEnd: handleCompositionEnd,
|
|
818
1221
|
onKeyDown: handleKeyDown,
|
|
819
1222
|
onMouseMove: handlePointerInput,
|
|
820
|
-
onMouseDown:
|
|
1223
|
+
onMouseDown: handleEditorMouseDown,
|
|
1224
|
+
onMouseUp: handleEditorMouseUp,
|
|
821
1225
|
onPointerMove: handlePointerInput,
|
|
822
1226
|
onCopy: handleCopy,
|
|
823
1227
|
onCut: handleCut,
|
|
@@ -29,8 +29,20 @@ export interface UseChipManagerReturn {
|
|
|
29
29
|
insertChip: (data: ChipData, editor: HTMLElement, preferredRange?: Range | null) => void;
|
|
30
30
|
/** 在指定 range 处插入 chip(调用方负责 deleteContents)。 */
|
|
31
31
|
insertChipAtRange: (data: ChipData, range: Range, editor: HTMLElement) => HTMLSpanElement;
|
|
32
|
-
/** 将含 marker 的 raw value + chips 插入 range
|
|
33
|
-
insertSerializedAtRange: (value: string, chips: ChipData[], range: Range, editor: HTMLElement
|
|
32
|
+
/** 将含 marker 的 raw value + chips 插入 range(粘贴 / 初始化还原用)。 */
|
|
33
|
+
insertSerializedAtRange: (value: string, chips: ChipData[], range: Range, editor: HTMLElement,
|
|
34
|
+
/**
|
|
35
|
+
* `focus: false` 时插入后不把焦点交给编辑器。
|
|
36
|
+
* 首屏还原 defaultValue / defaultChips 必须传 false:默认 focus() 会把
|
|
37
|
+
* 页面滚动到编辑器(浏览器「聚焦元素进入视口」),导致进页面就跳位置。
|
|
38
|
+
*
|
|
39
|
+
* `caret: false` 时不主动改写 selection(后台预填用,避免抢走用户
|
|
40
|
+
* 在别处的光标)。单行路径由调用方传自己的 range,不受影响。
|
|
41
|
+
*/
|
|
42
|
+
options?: {
|
|
43
|
+
focus?: boolean;
|
|
44
|
+
caret?: boolean;
|
|
45
|
+
}) => void;
|
|
34
46
|
/** 移除已从 DOM 摘下的 chip 状态(cut / 选区删除后)。 */
|
|
35
47
|
syncChipsAfterDomMutation: (editor: HTMLElement) => void;
|
|
36
48
|
/** 按 chip id 移除。 */
|