react-text-range 1.0.18 → 1.0.19
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/cjs/esm/useTextSelectionEditor.d.ts +7 -1
- package/dist/cjs/index.html +17 -0
- package/dist/cjs/index.js +167 -95
- package/dist/cjs/server.js +6 -0
- package/dist/esm/index.js +167 -95
- package/dist/esm/useTextSelectionEditor.d.ts +7 -1
- package/package.json +1 -1
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { HandlerPos } from './handler-pos';
|
|
3
|
+
interface CaretPosition {
|
|
4
|
+
readonly offsetNode: Node;
|
|
5
|
+
readonly offset: number;
|
|
6
|
+
getClientRect(): DOMRect | null;
|
|
7
|
+
}
|
|
3
8
|
declare global {
|
|
4
9
|
interface Document {
|
|
5
|
-
caretPositionFromPoint:
|
|
10
|
+
caretPositionFromPoint(x: number, y: number): CaretPosition | null;
|
|
6
11
|
}
|
|
7
12
|
}
|
|
8
13
|
export declare const useTextSelectionEditor: (text: string, initLeftPos: number, initRightPos: number, leftDrag: boolean, rightDrag: boolean, headClass?: string, selectionClass?: string, tailClass?: string) => [
|
|
@@ -10,3 +15,4 @@ export declare const useTextSelectionEditor: (text: string, initLeftPos: number,
|
|
|
10
15
|
HandlerPos | null,
|
|
11
16
|
HandlerPos | null
|
|
12
17
|
];
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Document</title>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script defer src="/index.js"></script>
|
|
13
|
+
</body>
|
|
14
|
+
|
|
15
|
+
</body>
|
|
16
|
+
|
|
17
|
+
</html>
|
package/dist/cjs/index.js
CHANGED
|
@@ -21,24 +21,42 @@ function _interopNamespaceDefault(e) {
|
|
|
21
21
|
|
|
22
22
|
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
|
|
23
23
|
|
|
24
|
+
// Fix 9: named constants for DOM child node structure
|
|
25
|
+
// After surroundContents: [text][span:head][text][span:selection][text][span:tail]
|
|
26
|
+
const EXPECTED_CHILD_COUNT = 6;
|
|
27
|
+
const NODE = { HEAD: 1, SEL: 3, TAIL: 5 };
|
|
28
|
+
// Fix 2: position clamping utility
|
|
29
|
+
const clampPositions = (left, right, textLen) => {
|
|
30
|
+
const clampedLeft = Math.max(0, Math.min(left, textLen));
|
|
31
|
+
const clampedRight = Math.max(clampedLeft, Math.min(right, textLen));
|
|
32
|
+
return [clampedLeft, clampedRight];
|
|
33
|
+
};
|
|
34
|
+
// Fix 8: shallow equality for HandlerPos to avoid unnecessary re-renders
|
|
35
|
+
const handlerPosEqual = (a, b) => {
|
|
36
|
+
if (a === b)
|
|
37
|
+
return true;
|
|
38
|
+
if (a === null || b === null)
|
|
39
|
+
return false;
|
|
40
|
+
return a.height === b.height && a.left === b.left && a.top === b.top && a.pos === b.pos;
|
|
41
|
+
};
|
|
24
42
|
const getHandlerRect = (node, left) => {
|
|
25
43
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
26
|
-
if (!node || !node.childNodes || node.childNodes.length
|
|
44
|
+
if (!node || !node.childNodes || node.childNodes.length !== EXPECTED_CHILD_COUNT)
|
|
27
45
|
return null;
|
|
28
|
-
const headLength = (_c = (_b = (_a = node.childNodes[
|
|
29
|
-
const selLength = (_f = (_e = (_d = node.childNodes[
|
|
30
|
-
const tailLength = (_j = (_h = (_g = node.childNodes[
|
|
46
|
+
const headLength = (_c = (_b = (_a = node.childNodes[NODE.HEAD].firstChild) === null || _a === void 0 ? void 0 : _a.nodeValue) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
|
|
47
|
+
const selLength = (_f = (_e = (_d = node.childNodes[NODE.SEL].firstChild) === null || _d === void 0 ? void 0 : _d.nodeValue) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0;
|
|
48
|
+
const tailLength = (_j = (_h = (_g = node.childNodes[NODE.TAIL].firstChild) === null || _g === void 0 ? void 0 : _g.nodeValue) === null || _h === void 0 ? void 0 : _h.length) !== null && _j !== void 0 ? _j : 0;
|
|
31
49
|
if (left) {
|
|
32
50
|
const range = document.createRange();
|
|
33
51
|
if (selLength > 0) {
|
|
34
|
-
range.selectNodeContents(node.childNodes[
|
|
52
|
+
range.selectNodeContents(node.childNodes[NODE.SEL]);
|
|
35
53
|
}
|
|
36
54
|
else if (tailLength > 0) {
|
|
37
|
-
range.selectNodeContents(node.childNodes[
|
|
55
|
+
range.selectNodeContents(node.childNodes[NODE.TAIL]);
|
|
38
56
|
}
|
|
39
57
|
else {
|
|
40
|
-
range.setStart(node.childNodes[
|
|
41
|
-
range.setEnd(node.childNodes[
|
|
58
|
+
range.setStart(node.childNodes[NODE.HEAD].childNodes[0], headLength);
|
|
59
|
+
range.setEnd(node.childNodes[NODE.HEAD].childNodes[0], headLength);
|
|
42
60
|
}
|
|
43
61
|
if (range.getClientRects) {
|
|
44
62
|
const rects = range.getClientRects();
|
|
@@ -51,15 +69,15 @@ const getHandlerRect = (node, left) => {
|
|
|
51
69
|
else {
|
|
52
70
|
const range = document.createRange();
|
|
53
71
|
if (tailLength > 0) {
|
|
54
|
-
range.selectNodeContents(node.childNodes[
|
|
72
|
+
range.selectNodeContents(node.childNodes[NODE.TAIL]);
|
|
55
73
|
}
|
|
56
74
|
else if (selLength > 0) {
|
|
57
|
-
range.setStart(node.childNodes[
|
|
58
|
-
range.setEnd(node.childNodes[
|
|
75
|
+
range.setStart(node.childNodes[NODE.SEL].childNodes[0], selLength);
|
|
76
|
+
range.setEnd(node.childNodes[NODE.SEL].childNodes[0], selLength);
|
|
59
77
|
}
|
|
60
78
|
else {
|
|
61
|
-
range.setStart(node.childNodes[
|
|
62
|
-
range.setEnd(node.childNodes[
|
|
79
|
+
range.setStart(node.childNodes[NODE.HEAD].childNodes[0], headLength);
|
|
80
|
+
range.setEnd(node.childNodes[NODE.HEAD].childNodes[0], headLength);
|
|
63
81
|
}
|
|
64
82
|
if (range.getClientRects) {
|
|
65
83
|
const rects = range.getClientRects();
|
|
@@ -71,61 +89,75 @@ const getHandlerRect = (node, left) => {
|
|
|
71
89
|
}
|
|
72
90
|
};
|
|
73
91
|
const createTextNodes = (div) => {
|
|
74
|
-
if (div.childNodes[
|
|
75
|
-
div.childNodes[
|
|
92
|
+
if (div.childNodes[NODE.HEAD] && !div.childNodes[NODE.HEAD].firstChild) {
|
|
93
|
+
div.childNodes[NODE.HEAD].appendChild(document.createTextNode(''));
|
|
76
94
|
}
|
|
77
|
-
if (div.childNodes[
|
|
78
|
-
div.childNodes[
|
|
95
|
+
if (div.childNodes[NODE.SEL] && !div.childNodes[NODE.SEL].firstChild) {
|
|
96
|
+
div.childNodes[NODE.SEL].appendChild(document.createTextNode(''));
|
|
79
97
|
}
|
|
80
|
-
if (div.childNodes[
|
|
81
|
-
div.childNodes[
|
|
98
|
+
if (div.childNodes[NODE.TAIL] && !div.childNodes[NODE.TAIL].firstChild) {
|
|
99
|
+
div.childNodes[NODE.TAIL].appendChild(document.createTextNode(''));
|
|
82
100
|
}
|
|
83
101
|
};
|
|
102
|
+
// Fix 11: proper types, null check on caretPositionFromPoint result
|
|
84
103
|
const getNodeAndOffsetFromPoint = (x, y) => {
|
|
85
|
-
|
|
86
|
-
let textNode;
|
|
87
|
-
let offset;
|
|
104
|
+
var _a, _b;
|
|
88
105
|
if (document.caretPositionFromPoint) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
range = document.caretRangeFromPoint(x, y);
|
|
95
|
-
if (range) {
|
|
96
|
-
textNode = range.startContainer;
|
|
97
|
-
offset = range.startOffset;
|
|
106
|
+
const caretPos = document.caretPositionFromPoint(x, y);
|
|
107
|
+
if (!caretPos)
|
|
108
|
+
return null;
|
|
109
|
+
if (((_a = caretPos.offsetNode) === null || _a === void 0 ? void 0 : _a.nodeType) === document.TEXT_NODE && !Number.isNaN(caretPos.offset)) {
|
|
110
|
+
return { node: caretPos.offsetNode, offset: caretPos.offset };
|
|
98
111
|
}
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
112
|
return null;
|
|
102
113
|
}
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
else if (document.caretRangeFromPoint) {
|
|
115
|
+
const range = document.caretRangeFromPoint(x, y);
|
|
116
|
+
if (range && ((_b = range.startContainer) === null || _b === void 0 ? void 0 : _b.nodeType) === document.TEXT_NODE && !Number.isNaN(range.startOffset)) {
|
|
117
|
+
return { node: range.startContainer, offset: range.startOffset };
|
|
106
118
|
}
|
|
119
|
+
return null;
|
|
107
120
|
}
|
|
108
121
|
return null;
|
|
109
122
|
};
|
|
110
123
|
const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, rightDrag, headClass, selectionClass, tailClass) => {
|
|
124
|
+
// Fix 10: SSR guard — useLayoutEffect won't run on server,
|
|
125
|
+
// but we still return safe defaults at the bottom
|
|
126
|
+
const isSSR = typeof document === 'undefined';
|
|
111
127
|
// left handler pos
|
|
112
128
|
const [leftHandler, setLeftHandler] = React.useState(null);
|
|
113
|
-
|
|
129
|
+
// Fix 2: clamp initial values
|
|
130
|
+
const [currentLeftPos, setCurrentLeftPos] = React.useState(() => {
|
|
131
|
+
const [cl] = clampPositions(initLeftPos, initRightPos, text.length);
|
|
132
|
+
return cl;
|
|
133
|
+
});
|
|
114
134
|
// right handler pos
|
|
115
135
|
const [rightHandler, setRightHandler] = React.useState(null);
|
|
116
|
-
const [currentRightPos, setCurrentRightPos] = React.useState(
|
|
136
|
+
const [currentRightPos, setCurrentRightPos] = React.useState(() => {
|
|
137
|
+
const [, cr] = clampPositions(initLeftPos, initRightPos, text.length);
|
|
138
|
+
return cr;
|
|
139
|
+
});
|
|
117
140
|
// reference
|
|
118
141
|
const textDiv = React.useRef(null);
|
|
142
|
+
// Fix 6: empty deps — ref is set after first render, useLayoutEffect runs after that
|
|
119
143
|
React.useLayoutEffect(() => {
|
|
120
144
|
if (textDiv.current) {
|
|
121
145
|
textDiv.current.style.position = 'relative';
|
|
122
146
|
}
|
|
123
|
-
}, [
|
|
124
|
-
//
|
|
147
|
+
}, []);
|
|
148
|
+
// Break text into three spans
|
|
149
|
+
// Fix 1: depend on props (initLeftPos/initRightPos) so DOM rebuilds when props change
|
|
150
|
+
// Fix 2: clamp positions before Range operations
|
|
125
151
|
React.useLayoutEffect(() => {
|
|
126
152
|
var _a, _b, _c;
|
|
153
|
+
if (isSSR)
|
|
154
|
+
return;
|
|
127
155
|
if (!textDiv.current)
|
|
128
156
|
return;
|
|
157
|
+
const [clampedLeft, clampedRight] = clampPositions(initLeftPos, initRightPos, text.length);
|
|
158
|
+
// Sync internal state to clamped prop values
|
|
159
|
+
setCurrentLeftPos(clampedLeft);
|
|
160
|
+
setCurrentRightPos(clampedRight);
|
|
129
161
|
// remove all nodes
|
|
130
162
|
while (textDiv.current.childNodes.length > 0 && textDiv.current.lastChild) {
|
|
131
163
|
textDiv.current.removeChild(textDiv.current.lastChild);
|
|
@@ -138,7 +170,7 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
138
170
|
}
|
|
139
171
|
const head = document.createRange();
|
|
140
172
|
head.setStart(textLeftNode, 0);
|
|
141
|
-
head.setEnd(textLeftNode,
|
|
173
|
+
head.setEnd(textLeftNode, clampedLeft);
|
|
142
174
|
const headSpan = document.createElement('span');
|
|
143
175
|
if (headClass)
|
|
144
176
|
headSpan.classList.value = headClass;
|
|
@@ -146,11 +178,11 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
146
178
|
textLeftNode = (_b = textDiv.current) === null || _b === void 0 ? void 0 : _b.childNodes[2];
|
|
147
179
|
if (!textLeftNode
|
|
148
180
|
|| !textLeftNode.nodeValue
|
|
149
|
-
|| textLeftNode.nodeValue.length <
|
|
181
|
+
|| textLeftNode.nodeValue.length < clampedRight - clampedLeft)
|
|
150
182
|
return;
|
|
151
183
|
const selection = document.createRange();
|
|
152
184
|
selection.setStart(textLeftNode, 0);
|
|
153
|
-
selection.setEnd(textLeftNode,
|
|
185
|
+
selection.setEnd(textLeftNode, clampedRight - clampedLeft);
|
|
154
186
|
const selectionSpan = document.createElement('span');
|
|
155
187
|
if (selectionClass)
|
|
156
188
|
selectionSpan.classList.value = selectionClass;
|
|
@@ -172,34 +204,44 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
172
204
|
}
|
|
173
205
|
}
|
|
174
206
|
};
|
|
175
|
-
}, [text]);
|
|
176
|
-
//
|
|
177
|
-
//
|
|
207
|
+
}, [text, initLeftPos, initRightPos, headClass, selectionClass, tailClass]);
|
|
208
|
+
// left handler drag
|
|
209
|
+
// Fix 4: null checks instead of non-null assertions
|
|
210
|
+
// Fix 6: remove textDiv.current from deps
|
|
178
211
|
const leftMoveHandler = React.useCallback((e) => {
|
|
212
|
+
var _a, _b;
|
|
179
213
|
const sm = getNodeAndOffsetFromPoint(e.clientX, e.clientY);
|
|
180
214
|
if (!sm)
|
|
181
215
|
return;
|
|
182
216
|
if (!textDiv.current)
|
|
183
217
|
return;
|
|
218
|
+
if (textDiv.current.childNodes.length !== EXPECTED_CHILD_COUNT)
|
|
219
|
+
return;
|
|
184
220
|
let posToSet = currentLeftPos;
|
|
185
|
-
if (sm.node === textDiv.current.childNodes[
|
|
221
|
+
if (sm.node === textDiv.current.childNodes[NODE.HEAD].firstChild) {
|
|
186
222
|
posToSet = sm.offset;
|
|
187
223
|
}
|
|
188
|
-
else if (sm.node === textDiv.current.childNodes[
|
|
224
|
+
else if (sm.node === textDiv.current.childNodes[NODE.SEL].firstChild) {
|
|
189
225
|
posToSet = currentLeftPos + sm.offset;
|
|
190
226
|
}
|
|
227
|
+
// Fix 2: clamp left pos to [0, currentRightPos]
|
|
228
|
+
posToSet = Math.max(0, Math.min(posToSet, currentRightPos));
|
|
191
229
|
if (posToSet !== currentLeftPos) {
|
|
192
230
|
createTextNodes(textDiv.current);
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
231
|
+
// Fix 4: null checks
|
|
232
|
+
const headTextNode = (_a = textDiv.current.childNodes[NODE.HEAD]) === null || _a === void 0 ? void 0 : _a.firstChild;
|
|
233
|
+
const selTextNode = (_b = textDiv.current.childNodes[NODE.SEL]) === null || _b === void 0 ? void 0 : _b.firstChild;
|
|
234
|
+
if (!headTextNode || !selTextNode
|
|
235
|
+
|| headTextNode.nodeValue === null || selTextNode.nodeValue === null) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const full = headTextNode.nodeValue + selTextNode.nodeValue;
|
|
239
|
+
headTextNode.nodeValue = full.substring(0, posToSet);
|
|
240
|
+
selTextNode.nodeValue = full.substring(posToSet);
|
|
200
241
|
setCurrentLeftPos(posToSet);
|
|
201
242
|
}
|
|
202
|
-
}, [currentLeftPos,
|
|
243
|
+
}, [currentLeftPos, currentRightPos, text]);
|
|
244
|
+
// Fix 6: depend on leftMoveHandler identity (which changes when its deps change)
|
|
203
245
|
React.useLayoutEffect(() => {
|
|
204
246
|
if (!leftDrag) {
|
|
205
247
|
document.removeEventListener('mousemove', leftMoveHandler);
|
|
@@ -210,11 +252,10 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
210
252
|
return () => {
|
|
211
253
|
document.removeEventListener('mousemove', leftMoveHandler);
|
|
212
254
|
};
|
|
213
|
-
}, [leftDrag,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// right handler
|
|
255
|
+
}, [leftDrag, leftMoveHandler]);
|
|
256
|
+
// right handler drag
|
|
257
|
+
// Fix 4: null checks instead of non-null assertions
|
|
258
|
+
// Fix 6: remove textDiv.current from deps
|
|
218
259
|
const rightMoveHandler = React.useCallback((e) => {
|
|
219
260
|
var _a, _b;
|
|
220
261
|
const sm = getNodeAndOffsetFromPoint(e.clientX, e.clientY);
|
|
@@ -222,25 +263,33 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
222
263
|
return;
|
|
223
264
|
if (!textDiv.current)
|
|
224
265
|
return;
|
|
266
|
+
if (textDiv.current.childNodes.length !== EXPECTED_CHILD_COUNT)
|
|
267
|
+
return;
|
|
225
268
|
let posToSet = currentRightPos;
|
|
226
|
-
if (sm.node ===
|
|
269
|
+
if (sm.node === textDiv.current.childNodes[NODE.SEL].firstChild) {
|
|
227
270
|
posToSet = currentLeftPos + sm.offset;
|
|
228
271
|
}
|
|
229
|
-
else if (sm.node ===
|
|
272
|
+
else if (sm.node === textDiv.current.childNodes[NODE.TAIL].firstChild) {
|
|
230
273
|
posToSet = currentRightPos + sm.offset;
|
|
231
274
|
}
|
|
275
|
+
// Fix 2: clamp right pos to [currentLeftPos, text.length]
|
|
276
|
+
posToSet = Math.max(currentLeftPos, Math.min(posToSet, text.length));
|
|
232
277
|
if (posToSet !== currentRightPos) {
|
|
233
278
|
createTextNodes(textDiv.current);
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
279
|
+
// Fix 4: null checks
|
|
280
|
+
const selTextNode = (_a = textDiv.current.childNodes[NODE.SEL]) === null || _a === void 0 ? void 0 : _a.firstChild;
|
|
281
|
+
const tailTextNode = (_b = textDiv.current.childNodes[NODE.TAIL]) === null || _b === void 0 ? void 0 : _b.firstChild;
|
|
282
|
+
if (!selTextNode || !tailTextNode
|
|
283
|
+
|| selTextNode.nodeValue === null || tailTextNode.nodeValue === null) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const full = selTextNode.nodeValue + tailTextNode.nodeValue;
|
|
287
|
+
selTextNode.nodeValue = full.substring(0, posToSet - currentLeftPos);
|
|
288
|
+
tailTextNode.nodeValue = full.substring(posToSet - currentLeftPos);
|
|
241
289
|
setCurrentRightPos(posToSet);
|
|
242
290
|
}
|
|
243
|
-
}, [currentLeftPos, currentRightPos,
|
|
291
|
+
}, [currentLeftPos, currentRightPos, text]);
|
|
292
|
+
// Fix 6: depend on rightMoveHandler identity
|
|
244
293
|
React.useLayoutEffect(() => {
|
|
245
294
|
if (!rightDrag) {
|
|
246
295
|
document.removeEventListener('mousemove', rightMoveHandler);
|
|
@@ -251,49 +300,53 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
251
300
|
return () => {
|
|
252
301
|
document.removeEventListener('mousemove', rightMoveHandler);
|
|
253
302
|
};
|
|
254
|
-
}, [rightDrag,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}, [initRightPos]);
|
|
258
|
-
// draw init left handler
|
|
303
|
+
}, [rightDrag, rightMoveHandler]);
|
|
304
|
+
// draw left handler position
|
|
305
|
+
// Fix 8: shallow equality check before setting state
|
|
259
306
|
React.useLayoutEffect(() => {
|
|
260
307
|
if (textDiv.current
|
|
261
|
-
&& textDiv.current.childNodes.length ===
|
|
308
|
+
&& textDiv.current.childNodes.length === EXPECTED_CHILD_COUNT) {
|
|
262
309
|
const rect = getHandlerRect(textDiv.current, true);
|
|
263
310
|
if (rect === null) {
|
|
264
|
-
setLeftHandler(null);
|
|
311
|
+
setLeftHandler(prev => prev === null ? prev : null);
|
|
265
312
|
}
|
|
266
313
|
else {
|
|
267
314
|
const divRect = textDiv.current.getBoundingClientRect();
|
|
268
|
-
|
|
315
|
+
const newPos = {
|
|
269
316
|
height: rect.height,
|
|
270
317
|
left: rect.left - divRect.left,
|
|
271
318
|
top: rect.top - divRect.top,
|
|
272
319
|
pos: currentLeftPos,
|
|
273
|
-
}
|
|
320
|
+
};
|
|
321
|
+
setLeftHandler(prev => handlerPosEqual(prev, newPos) ? prev : newPos);
|
|
274
322
|
}
|
|
275
323
|
}
|
|
276
324
|
}, [currentLeftPos]);
|
|
277
|
-
// draw
|
|
325
|
+
// draw right handler position
|
|
326
|
+
// Fix 8: shallow equality check before setting state
|
|
278
327
|
React.useLayoutEffect(() => {
|
|
279
328
|
if (textDiv.current
|
|
280
|
-
&& textDiv.current.childNodes.length ===
|
|
329
|
+
&& textDiv.current.childNodes.length === EXPECTED_CHILD_COUNT) {
|
|
281
330
|
const rect = getHandlerRect(textDiv.current, false);
|
|
282
331
|
if (rect === null) {
|
|
283
|
-
setRightHandler(null);
|
|
332
|
+
setRightHandler(prev => prev === null ? prev : null);
|
|
284
333
|
}
|
|
285
334
|
else {
|
|
286
335
|
const divRect = textDiv.current.getBoundingClientRect();
|
|
287
|
-
|
|
336
|
+
const newPos = {
|
|
288
337
|
height: rect.height,
|
|
289
338
|
left: rect.left - divRect.left,
|
|
290
339
|
top: rect.top - divRect.top,
|
|
291
340
|
pos: currentRightPos,
|
|
292
|
-
}
|
|
341
|
+
};
|
|
342
|
+
setRightHandler(prev => handlerPosEqual(prev, newPos) ? prev : newPos);
|
|
293
343
|
}
|
|
294
344
|
}
|
|
295
345
|
}, [currentRightPos]);
|
|
296
|
-
// return
|
|
346
|
+
// Fix 10: return safe defaults for SSR
|
|
347
|
+
if (isSSR) {
|
|
348
|
+
return [textDiv, null, null];
|
|
349
|
+
}
|
|
297
350
|
return [textDiv, leftHandler, rightHandler];
|
|
298
351
|
};
|
|
299
352
|
|
|
@@ -351,6 +404,29 @@ styleInject(css_248z,{"insertAt":"top"});
|
|
|
351
404
|
|
|
352
405
|
const SelectionHandler = ({ pos, grab, setGrab, left, width, className }) => {
|
|
353
406
|
const widthDef = width !== null && width !== void 0 ? width : 25;
|
|
407
|
+
// Fix 3: track mouseup handler in ref for cleanup on unmount
|
|
408
|
+
const mouseUpHandlerRef = React.useRef(null);
|
|
409
|
+
React.useEffect(() => {
|
|
410
|
+
return () => {
|
|
411
|
+
if (mouseUpHandlerRef.current) {
|
|
412
|
+
document.removeEventListener('mouseup', mouseUpHandlerRef.current);
|
|
413
|
+
mouseUpHandlerRef.current = null;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}, []);
|
|
417
|
+
const handleMouseDown = () => {
|
|
418
|
+
setGrab(true);
|
|
419
|
+
if (mouseUpHandlerRef.current) {
|
|
420
|
+
document.removeEventListener('mouseup', mouseUpHandlerRef.current);
|
|
421
|
+
}
|
|
422
|
+
const handler = () => {
|
|
423
|
+
setGrab(false);
|
|
424
|
+
document.removeEventListener('mouseup', handler);
|
|
425
|
+
mouseUpHandlerRef.current = null;
|
|
426
|
+
};
|
|
427
|
+
mouseUpHandlerRef.current = handler;
|
|
428
|
+
document.addEventListener('mouseup', handler);
|
|
429
|
+
};
|
|
354
430
|
return (pos &&
|
|
355
431
|
React.createElement("div", { draggable: false, className: `${left ? 'rounded-l-md' : 'rounded-r-md'} ${className}`, style: {
|
|
356
432
|
position: 'absolute',
|
|
@@ -361,14 +437,7 @@ const SelectionHandler = ({ pos, grab, setGrab, left, width, className }) => {
|
|
|
361
437
|
height: pos.height,
|
|
362
438
|
cursor: grab ? 'grabbing' : 'grab',
|
|
363
439
|
alignItems: left ? 'flex-start' : 'flex-end',
|
|
364
|
-
}, onMouseDown: () => {
|
|
365
|
-
setGrab(true);
|
|
366
|
-
const handler = () => {
|
|
367
|
-
setGrab(false);
|
|
368
|
-
document.removeEventListener('mouseup', handler);
|
|
369
|
-
};
|
|
370
|
-
document.addEventListener('mouseup', handler);
|
|
371
|
-
}, onMouseUp: () => {
|
|
440
|
+
}, onMouseDown: handleMouseDown, onMouseUp: () => {
|
|
372
441
|
setGrab(false);
|
|
373
442
|
} }, left
|
|
374
443
|
? React.createElement(SvgQuoteLeft, null)
|
|
@@ -379,9 +448,12 @@ const ReactTextRange = ({ initLeftPos, initRightPos, Container, text, onChange,
|
|
|
379
448
|
const [mouseOnLeft, setMouseOnLeft] = React.useState(false);
|
|
380
449
|
const [mouseOnRight, setMouseOnRight] = React.useState(false);
|
|
381
450
|
const [textDiv, leftHandler, rightHandler] = useTextSelectionEditor(text, initLeftPos, initRightPos, mouseOnLeft, mouseOnRight, headClass, selectionClass, tailClass);
|
|
451
|
+
// Fix 7: use ref to always call the latest onChange without adding it to deps
|
|
452
|
+
const onChangeRef = React.useRef(onChange);
|
|
453
|
+
onChangeRef.current = onChange;
|
|
382
454
|
React.useEffect(() => {
|
|
383
455
|
if (leftHandler && rightHandler) {
|
|
384
|
-
|
|
456
|
+
onChangeRef.current({
|
|
385
457
|
left: leftHandler.pos,
|
|
386
458
|
right: rightHandler.pos,
|
|
387
459
|
});
|
package/dist/esm/index.js
CHANGED
|
@@ -1,24 +1,42 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import React__default, { useState, useRef, useLayoutEffect, useCallback, useEffect } from 'react';
|
|
3
3
|
|
|
4
|
+
// Fix 9: named constants for DOM child node structure
|
|
5
|
+
// After surroundContents: [text][span:head][text][span:selection][text][span:tail]
|
|
6
|
+
const EXPECTED_CHILD_COUNT = 6;
|
|
7
|
+
const NODE = { HEAD: 1, SEL: 3, TAIL: 5 };
|
|
8
|
+
// Fix 2: position clamping utility
|
|
9
|
+
const clampPositions = (left, right, textLen) => {
|
|
10
|
+
const clampedLeft = Math.max(0, Math.min(left, textLen));
|
|
11
|
+
const clampedRight = Math.max(clampedLeft, Math.min(right, textLen));
|
|
12
|
+
return [clampedLeft, clampedRight];
|
|
13
|
+
};
|
|
14
|
+
// Fix 8: shallow equality for HandlerPos to avoid unnecessary re-renders
|
|
15
|
+
const handlerPosEqual = (a, b) => {
|
|
16
|
+
if (a === b)
|
|
17
|
+
return true;
|
|
18
|
+
if (a === null || b === null)
|
|
19
|
+
return false;
|
|
20
|
+
return a.height === b.height && a.left === b.left && a.top === b.top && a.pos === b.pos;
|
|
21
|
+
};
|
|
4
22
|
const getHandlerRect = (node, left) => {
|
|
5
23
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
6
|
-
if (!node || !node.childNodes || node.childNodes.length
|
|
24
|
+
if (!node || !node.childNodes || node.childNodes.length !== EXPECTED_CHILD_COUNT)
|
|
7
25
|
return null;
|
|
8
|
-
const headLength = (_c = (_b = (_a = node.childNodes[
|
|
9
|
-
const selLength = (_f = (_e = (_d = node.childNodes[
|
|
10
|
-
const tailLength = (_j = (_h = (_g = node.childNodes[
|
|
26
|
+
const headLength = (_c = (_b = (_a = node.childNodes[NODE.HEAD].firstChild) === null || _a === void 0 ? void 0 : _a.nodeValue) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
|
|
27
|
+
const selLength = (_f = (_e = (_d = node.childNodes[NODE.SEL].firstChild) === null || _d === void 0 ? void 0 : _d.nodeValue) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0;
|
|
28
|
+
const tailLength = (_j = (_h = (_g = node.childNodes[NODE.TAIL].firstChild) === null || _g === void 0 ? void 0 : _g.nodeValue) === null || _h === void 0 ? void 0 : _h.length) !== null && _j !== void 0 ? _j : 0;
|
|
11
29
|
if (left) {
|
|
12
30
|
const range = document.createRange();
|
|
13
31
|
if (selLength > 0) {
|
|
14
|
-
range.selectNodeContents(node.childNodes[
|
|
32
|
+
range.selectNodeContents(node.childNodes[NODE.SEL]);
|
|
15
33
|
}
|
|
16
34
|
else if (tailLength > 0) {
|
|
17
|
-
range.selectNodeContents(node.childNodes[
|
|
35
|
+
range.selectNodeContents(node.childNodes[NODE.TAIL]);
|
|
18
36
|
}
|
|
19
37
|
else {
|
|
20
|
-
range.setStart(node.childNodes[
|
|
21
|
-
range.setEnd(node.childNodes[
|
|
38
|
+
range.setStart(node.childNodes[NODE.HEAD].childNodes[0], headLength);
|
|
39
|
+
range.setEnd(node.childNodes[NODE.HEAD].childNodes[0], headLength);
|
|
22
40
|
}
|
|
23
41
|
if (range.getClientRects) {
|
|
24
42
|
const rects = range.getClientRects();
|
|
@@ -31,15 +49,15 @@ const getHandlerRect = (node, left) => {
|
|
|
31
49
|
else {
|
|
32
50
|
const range = document.createRange();
|
|
33
51
|
if (tailLength > 0) {
|
|
34
|
-
range.selectNodeContents(node.childNodes[
|
|
52
|
+
range.selectNodeContents(node.childNodes[NODE.TAIL]);
|
|
35
53
|
}
|
|
36
54
|
else if (selLength > 0) {
|
|
37
|
-
range.setStart(node.childNodes[
|
|
38
|
-
range.setEnd(node.childNodes[
|
|
55
|
+
range.setStart(node.childNodes[NODE.SEL].childNodes[0], selLength);
|
|
56
|
+
range.setEnd(node.childNodes[NODE.SEL].childNodes[0], selLength);
|
|
39
57
|
}
|
|
40
58
|
else {
|
|
41
|
-
range.setStart(node.childNodes[
|
|
42
|
-
range.setEnd(node.childNodes[
|
|
59
|
+
range.setStart(node.childNodes[NODE.HEAD].childNodes[0], headLength);
|
|
60
|
+
range.setEnd(node.childNodes[NODE.HEAD].childNodes[0], headLength);
|
|
43
61
|
}
|
|
44
62
|
if (range.getClientRects) {
|
|
45
63
|
const rects = range.getClientRects();
|
|
@@ -51,61 +69,75 @@ const getHandlerRect = (node, left) => {
|
|
|
51
69
|
}
|
|
52
70
|
};
|
|
53
71
|
const createTextNodes = (div) => {
|
|
54
|
-
if (div.childNodes[
|
|
55
|
-
div.childNodes[
|
|
72
|
+
if (div.childNodes[NODE.HEAD] && !div.childNodes[NODE.HEAD].firstChild) {
|
|
73
|
+
div.childNodes[NODE.HEAD].appendChild(document.createTextNode(''));
|
|
56
74
|
}
|
|
57
|
-
if (div.childNodes[
|
|
58
|
-
div.childNodes[
|
|
75
|
+
if (div.childNodes[NODE.SEL] && !div.childNodes[NODE.SEL].firstChild) {
|
|
76
|
+
div.childNodes[NODE.SEL].appendChild(document.createTextNode(''));
|
|
59
77
|
}
|
|
60
|
-
if (div.childNodes[
|
|
61
|
-
div.childNodes[
|
|
78
|
+
if (div.childNodes[NODE.TAIL] && !div.childNodes[NODE.TAIL].firstChild) {
|
|
79
|
+
div.childNodes[NODE.TAIL].appendChild(document.createTextNode(''));
|
|
62
80
|
}
|
|
63
81
|
};
|
|
82
|
+
// Fix 11: proper types, null check on caretPositionFromPoint result
|
|
64
83
|
const getNodeAndOffsetFromPoint = (x, y) => {
|
|
65
|
-
|
|
66
|
-
let textNode;
|
|
67
|
-
let offset;
|
|
84
|
+
var _a, _b;
|
|
68
85
|
if (document.caretPositionFromPoint) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
range = document.caretRangeFromPoint(x, y);
|
|
75
|
-
if (range) {
|
|
76
|
-
textNode = range.startContainer;
|
|
77
|
-
offset = range.startOffset;
|
|
86
|
+
const caretPos = document.caretPositionFromPoint(x, y);
|
|
87
|
+
if (!caretPos)
|
|
88
|
+
return null;
|
|
89
|
+
if (((_a = caretPos.offsetNode) === null || _a === void 0 ? void 0 : _a.nodeType) === document.TEXT_NODE && !Number.isNaN(caretPos.offset)) {
|
|
90
|
+
return { node: caretPos.offsetNode, offset: caretPos.offset };
|
|
78
91
|
}
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
92
|
return null;
|
|
82
93
|
}
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
else if (document.caretRangeFromPoint) {
|
|
95
|
+
const range = document.caretRangeFromPoint(x, y);
|
|
96
|
+
if (range && ((_b = range.startContainer) === null || _b === void 0 ? void 0 : _b.nodeType) === document.TEXT_NODE && !Number.isNaN(range.startOffset)) {
|
|
97
|
+
return { node: range.startContainer, offset: range.startOffset };
|
|
86
98
|
}
|
|
99
|
+
return null;
|
|
87
100
|
}
|
|
88
101
|
return null;
|
|
89
102
|
};
|
|
90
103
|
const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, rightDrag, headClass, selectionClass, tailClass) => {
|
|
104
|
+
// Fix 10: SSR guard — useLayoutEffect won't run on server,
|
|
105
|
+
// but we still return safe defaults at the bottom
|
|
106
|
+
const isSSR = typeof document === 'undefined';
|
|
91
107
|
// left handler pos
|
|
92
108
|
const [leftHandler, setLeftHandler] = useState(null);
|
|
93
|
-
|
|
109
|
+
// Fix 2: clamp initial values
|
|
110
|
+
const [currentLeftPos, setCurrentLeftPos] = useState(() => {
|
|
111
|
+
const [cl] = clampPositions(initLeftPos, initRightPos, text.length);
|
|
112
|
+
return cl;
|
|
113
|
+
});
|
|
94
114
|
// right handler pos
|
|
95
115
|
const [rightHandler, setRightHandler] = useState(null);
|
|
96
|
-
const [currentRightPos, setCurrentRightPos] = useState(
|
|
116
|
+
const [currentRightPos, setCurrentRightPos] = useState(() => {
|
|
117
|
+
const [, cr] = clampPositions(initLeftPos, initRightPos, text.length);
|
|
118
|
+
return cr;
|
|
119
|
+
});
|
|
97
120
|
// reference
|
|
98
121
|
const textDiv = useRef(null);
|
|
122
|
+
// Fix 6: empty deps — ref is set after first render, useLayoutEffect runs after that
|
|
99
123
|
useLayoutEffect(() => {
|
|
100
124
|
if (textDiv.current) {
|
|
101
125
|
textDiv.current.style.position = 'relative';
|
|
102
126
|
}
|
|
103
|
-
}, [
|
|
104
|
-
//
|
|
127
|
+
}, []);
|
|
128
|
+
// Break text into three spans
|
|
129
|
+
// Fix 1: depend on props (initLeftPos/initRightPos) so DOM rebuilds when props change
|
|
130
|
+
// Fix 2: clamp positions before Range operations
|
|
105
131
|
useLayoutEffect(() => {
|
|
106
132
|
var _a, _b, _c;
|
|
133
|
+
if (isSSR)
|
|
134
|
+
return;
|
|
107
135
|
if (!textDiv.current)
|
|
108
136
|
return;
|
|
137
|
+
const [clampedLeft, clampedRight] = clampPositions(initLeftPos, initRightPos, text.length);
|
|
138
|
+
// Sync internal state to clamped prop values
|
|
139
|
+
setCurrentLeftPos(clampedLeft);
|
|
140
|
+
setCurrentRightPos(clampedRight);
|
|
109
141
|
// remove all nodes
|
|
110
142
|
while (textDiv.current.childNodes.length > 0 && textDiv.current.lastChild) {
|
|
111
143
|
textDiv.current.removeChild(textDiv.current.lastChild);
|
|
@@ -118,7 +150,7 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
118
150
|
}
|
|
119
151
|
const head = document.createRange();
|
|
120
152
|
head.setStart(textLeftNode, 0);
|
|
121
|
-
head.setEnd(textLeftNode,
|
|
153
|
+
head.setEnd(textLeftNode, clampedLeft);
|
|
122
154
|
const headSpan = document.createElement('span');
|
|
123
155
|
if (headClass)
|
|
124
156
|
headSpan.classList.value = headClass;
|
|
@@ -126,11 +158,11 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
126
158
|
textLeftNode = (_b = textDiv.current) === null || _b === void 0 ? void 0 : _b.childNodes[2];
|
|
127
159
|
if (!textLeftNode
|
|
128
160
|
|| !textLeftNode.nodeValue
|
|
129
|
-
|| textLeftNode.nodeValue.length <
|
|
161
|
+
|| textLeftNode.nodeValue.length < clampedRight - clampedLeft)
|
|
130
162
|
return;
|
|
131
163
|
const selection = document.createRange();
|
|
132
164
|
selection.setStart(textLeftNode, 0);
|
|
133
|
-
selection.setEnd(textLeftNode,
|
|
165
|
+
selection.setEnd(textLeftNode, clampedRight - clampedLeft);
|
|
134
166
|
const selectionSpan = document.createElement('span');
|
|
135
167
|
if (selectionClass)
|
|
136
168
|
selectionSpan.classList.value = selectionClass;
|
|
@@ -152,34 +184,44 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
152
184
|
}
|
|
153
185
|
}
|
|
154
186
|
};
|
|
155
|
-
}, [text]);
|
|
156
|
-
//
|
|
157
|
-
//
|
|
187
|
+
}, [text, initLeftPos, initRightPos, headClass, selectionClass, tailClass]);
|
|
188
|
+
// left handler drag
|
|
189
|
+
// Fix 4: null checks instead of non-null assertions
|
|
190
|
+
// Fix 6: remove textDiv.current from deps
|
|
158
191
|
const leftMoveHandler = useCallback((e) => {
|
|
192
|
+
var _a, _b;
|
|
159
193
|
const sm = getNodeAndOffsetFromPoint(e.clientX, e.clientY);
|
|
160
194
|
if (!sm)
|
|
161
195
|
return;
|
|
162
196
|
if (!textDiv.current)
|
|
163
197
|
return;
|
|
198
|
+
if (textDiv.current.childNodes.length !== EXPECTED_CHILD_COUNT)
|
|
199
|
+
return;
|
|
164
200
|
let posToSet = currentLeftPos;
|
|
165
|
-
if (sm.node === textDiv.current.childNodes[
|
|
201
|
+
if (sm.node === textDiv.current.childNodes[NODE.HEAD].firstChild) {
|
|
166
202
|
posToSet = sm.offset;
|
|
167
203
|
}
|
|
168
|
-
else if (sm.node === textDiv.current.childNodes[
|
|
204
|
+
else if (sm.node === textDiv.current.childNodes[NODE.SEL].firstChild) {
|
|
169
205
|
posToSet = currentLeftPos + sm.offset;
|
|
170
206
|
}
|
|
207
|
+
// Fix 2: clamp left pos to [0, currentRightPos]
|
|
208
|
+
posToSet = Math.max(0, Math.min(posToSet, currentRightPos));
|
|
171
209
|
if (posToSet !== currentLeftPos) {
|
|
172
210
|
createTextNodes(textDiv.current);
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
211
|
+
// Fix 4: null checks
|
|
212
|
+
const headTextNode = (_a = textDiv.current.childNodes[NODE.HEAD]) === null || _a === void 0 ? void 0 : _a.firstChild;
|
|
213
|
+
const selTextNode = (_b = textDiv.current.childNodes[NODE.SEL]) === null || _b === void 0 ? void 0 : _b.firstChild;
|
|
214
|
+
if (!headTextNode || !selTextNode
|
|
215
|
+
|| headTextNode.nodeValue === null || selTextNode.nodeValue === null) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const full = headTextNode.nodeValue + selTextNode.nodeValue;
|
|
219
|
+
headTextNode.nodeValue = full.substring(0, posToSet);
|
|
220
|
+
selTextNode.nodeValue = full.substring(posToSet);
|
|
180
221
|
setCurrentLeftPos(posToSet);
|
|
181
222
|
}
|
|
182
|
-
}, [currentLeftPos,
|
|
223
|
+
}, [currentLeftPos, currentRightPos, text]);
|
|
224
|
+
// Fix 6: depend on leftMoveHandler identity (which changes when its deps change)
|
|
183
225
|
useLayoutEffect(() => {
|
|
184
226
|
if (!leftDrag) {
|
|
185
227
|
document.removeEventListener('mousemove', leftMoveHandler);
|
|
@@ -190,11 +232,10 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
190
232
|
return () => {
|
|
191
233
|
document.removeEventListener('mousemove', leftMoveHandler);
|
|
192
234
|
};
|
|
193
|
-
}, [leftDrag,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// right handler
|
|
235
|
+
}, [leftDrag, leftMoveHandler]);
|
|
236
|
+
// right handler drag
|
|
237
|
+
// Fix 4: null checks instead of non-null assertions
|
|
238
|
+
// Fix 6: remove textDiv.current from deps
|
|
198
239
|
const rightMoveHandler = useCallback((e) => {
|
|
199
240
|
var _a, _b;
|
|
200
241
|
const sm = getNodeAndOffsetFromPoint(e.clientX, e.clientY);
|
|
@@ -202,25 +243,33 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
202
243
|
return;
|
|
203
244
|
if (!textDiv.current)
|
|
204
245
|
return;
|
|
246
|
+
if (textDiv.current.childNodes.length !== EXPECTED_CHILD_COUNT)
|
|
247
|
+
return;
|
|
205
248
|
let posToSet = currentRightPos;
|
|
206
|
-
if (sm.node ===
|
|
249
|
+
if (sm.node === textDiv.current.childNodes[NODE.SEL].firstChild) {
|
|
207
250
|
posToSet = currentLeftPos + sm.offset;
|
|
208
251
|
}
|
|
209
|
-
else if (sm.node ===
|
|
252
|
+
else if (sm.node === textDiv.current.childNodes[NODE.TAIL].firstChild) {
|
|
210
253
|
posToSet = currentRightPos + sm.offset;
|
|
211
254
|
}
|
|
255
|
+
// Fix 2: clamp right pos to [currentLeftPos, text.length]
|
|
256
|
+
posToSet = Math.max(currentLeftPos, Math.min(posToSet, text.length));
|
|
212
257
|
if (posToSet !== currentRightPos) {
|
|
213
258
|
createTextNodes(textDiv.current);
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
259
|
+
// Fix 4: null checks
|
|
260
|
+
const selTextNode = (_a = textDiv.current.childNodes[NODE.SEL]) === null || _a === void 0 ? void 0 : _a.firstChild;
|
|
261
|
+
const tailTextNode = (_b = textDiv.current.childNodes[NODE.TAIL]) === null || _b === void 0 ? void 0 : _b.firstChild;
|
|
262
|
+
if (!selTextNode || !tailTextNode
|
|
263
|
+
|| selTextNode.nodeValue === null || tailTextNode.nodeValue === null) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const full = selTextNode.nodeValue + tailTextNode.nodeValue;
|
|
267
|
+
selTextNode.nodeValue = full.substring(0, posToSet - currentLeftPos);
|
|
268
|
+
tailTextNode.nodeValue = full.substring(posToSet - currentLeftPos);
|
|
221
269
|
setCurrentRightPos(posToSet);
|
|
222
270
|
}
|
|
223
|
-
}, [currentLeftPos, currentRightPos,
|
|
271
|
+
}, [currentLeftPos, currentRightPos, text]);
|
|
272
|
+
// Fix 6: depend on rightMoveHandler identity
|
|
224
273
|
useLayoutEffect(() => {
|
|
225
274
|
if (!rightDrag) {
|
|
226
275
|
document.removeEventListener('mousemove', rightMoveHandler);
|
|
@@ -231,49 +280,53 @@ const useTextSelectionEditor = (text, initLeftPos, initRightPos, leftDrag, right
|
|
|
231
280
|
return () => {
|
|
232
281
|
document.removeEventListener('mousemove', rightMoveHandler);
|
|
233
282
|
};
|
|
234
|
-
}, [rightDrag,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}, [initRightPos]);
|
|
238
|
-
// draw init left handler
|
|
283
|
+
}, [rightDrag, rightMoveHandler]);
|
|
284
|
+
// draw left handler position
|
|
285
|
+
// Fix 8: shallow equality check before setting state
|
|
239
286
|
useLayoutEffect(() => {
|
|
240
287
|
if (textDiv.current
|
|
241
|
-
&& textDiv.current.childNodes.length ===
|
|
288
|
+
&& textDiv.current.childNodes.length === EXPECTED_CHILD_COUNT) {
|
|
242
289
|
const rect = getHandlerRect(textDiv.current, true);
|
|
243
290
|
if (rect === null) {
|
|
244
|
-
setLeftHandler(null);
|
|
291
|
+
setLeftHandler(prev => prev === null ? prev : null);
|
|
245
292
|
}
|
|
246
293
|
else {
|
|
247
294
|
const divRect = textDiv.current.getBoundingClientRect();
|
|
248
|
-
|
|
295
|
+
const newPos = {
|
|
249
296
|
height: rect.height,
|
|
250
297
|
left: rect.left - divRect.left,
|
|
251
298
|
top: rect.top - divRect.top,
|
|
252
299
|
pos: currentLeftPos,
|
|
253
|
-
}
|
|
300
|
+
};
|
|
301
|
+
setLeftHandler(prev => handlerPosEqual(prev, newPos) ? prev : newPos);
|
|
254
302
|
}
|
|
255
303
|
}
|
|
256
304
|
}, [currentLeftPos]);
|
|
257
|
-
// draw
|
|
305
|
+
// draw right handler position
|
|
306
|
+
// Fix 8: shallow equality check before setting state
|
|
258
307
|
useLayoutEffect(() => {
|
|
259
308
|
if (textDiv.current
|
|
260
|
-
&& textDiv.current.childNodes.length ===
|
|
309
|
+
&& textDiv.current.childNodes.length === EXPECTED_CHILD_COUNT) {
|
|
261
310
|
const rect = getHandlerRect(textDiv.current, false);
|
|
262
311
|
if (rect === null) {
|
|
263
|
-
setRightHandler(null);
|
|
312
|
+
setRightHandler(prev => prev === null ? prev : null);
|
|
264
313
|
}
|
|
265
314
|
else {
|
|
266
315
|
const divRect = textDiv.current.getBoundingClientRect();
|
|
267
|
-
|
|
316
|
+
const newPos = {
|
|
268
317
|
height: rect.height,
|
|
269
318
|
left: rect.left - divRect.left,
|
|
270
319
|
top: rect.top - divRect.top,
|
|
271
320
|
pos: currentRightPos,
|
|
272
|
-
}
|
|
321
|
+
};
|
|
322
|
+
setRightHandler(prev => handlerPosEqual(prev, newPos) ? prev : newPos);
|
|
273
323
|
}
|
|
274
324
|
}
|
|
275
325
|
}, [currentRightPos]);
|
|
276
|
-
// return
|
|
326
|
+
// Fix 10: return safe defaults for SSR
|
|
327
|
+
if (isSSR) {
|
|
328
|
+
return [textDiv, null, null];
|
|
329
|
+
}
|
|
277
330
|
return [textDiv, leftHandler, rightHandler];
|
|
278
331
|
};
|
|
279
332
|
|
|
@@ -331,6 +384,29 @@ styleInject(css_248z,{"insertAt":"top"});
|
|
|
331
384
|
|
|
332
385
|
const SelectionHandler = ({ pos, grab, setGrab, left, width, className }) => {
|
|
333
386
|
const widthDef = width !== null && width !== void 0 ? width : 25;
|
|
387
|
+
// Fix 3: track mouseup handler in ref for cleanup on unmount
|
|
388
|
+
const mouseUpHandlerRef = useRef(null);
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
return () => {
|
|
391
|
+
if (mouseUpHandlerRef.current) {
|
|
392
|
+
document.removeEventListener('mouseup', mouseUpHandlerRef.current);
|
|
393
|
+
mouseUpHandlerRef.current = null;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}, []);
|
|
397
|
+
const handleMouseDown = () => {
|
|
398
|
+
setGrab(true);
|
|
399
|
+
if (mouseUpHandlerRef.current) {
|
|
400
|
+
document.removeEventListener('mouseup', mouseUpHandlerRef.current);
|
|
401
|
+
}
|
|
402
|
+
const handler = () => {
|
|
403
|
+
setGrab(false);
|
|
404
|
+
document.removeEventListener('mouseup', handler);
|
|
405
|
+
mouseUpHandlerRef.current = null;
|
|
406
|
+
};
|
|
407
|
+
mouseUpHandlerRef.current = handler;
|
|
408
|
+
document.addEventListener('mouseup', handler);
|
|
409
|
+
};
|
|
334
410
|
return (pos &&
|
|
335
411
|
React__default.createElement("div", { draggable: false, className: `${left ? 'rounded-l-md' : 'rounded-r-md'} ${className}`, style: {
|
|
336
412
|
position: 'absolute',
|
|
@@ -341,14 +417,7 @@ const SelectionHandler = ({ pos, grab, setGrab, left, width, className }) => {
|
|
|
341
417
|
height: pos.height,
|
|
342
418
|
cursor: grab ? 'grabbing' : 'grab',
|
|
343
419
|
alignItems: left ? 'flex-start' : 'flex-end',
|
|
344
|
-
}, onMouseDown: () => {
|
|
345
|
-
setGrab(true);
|
|
346
|
-
const handler = () => {
|
|
347
|
-
setGrab(false);
|
|
348
|
-
document.removeEventListener('mouseup', handler);
|
|
349
|
-
};
|
|
350
|
-
document.addEventListener('mouseup', handler);
|
|
351
|
-
}, onMouseUp: () => {
|
|
420
|
+
}, onMouseDown: handleMouseDown, onMouseUp: () => {
|
|
352
421
|
setGrab(false);
|
|
353
422
|
} }, left
|
|
354
423
|
? React__default.createElement(SvgQuoteLeft, null)
|
|
@@ -359,9 +428,12 @@ const ReactTextRange = ({ initLeftPos, initRightPos, Container, text, onChange,
|
|
|
359
428
|
const [mouseOnLeft, setMouseOnLeft] = useState(false);
|
|
360
429
|
const [mouseOnRight, setMouseOnRight] = useState(false);
|
|
361
430
|
const [textDiv, leftHandler, rightHandler] = useTextSelectionEditor(text, initLeftPos, initRightPos, mouseOnLeft, mouseOnRight, headClass, selectionClass, tailClass);
|
|
431
|
+
// Fix 7: use ref to always call the latest onChange without adding it to deps
|
|
432
|
+
const onChangeRef = useRef(onChange);
|
|
433
|
+
onChangeRef.current = onChange;
|
|
362
434
|
useEffect(() => {
|
|
363
435
|
if (leftHandler && rightHandler) {
|
|
364
|
-
|
|
436
|
+
onChangeRef.current({
|
|
365
437
|
left: leftHandler.pos,
|
|
366
438
|
right: rightHandler.pos,
|
|
367
439
|
});
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { HandlerPos } from './handler-pos';
|
|
3
|
+
interface CaretPosition {
|
|
4
|
+
readonly offsetNode: Node;
|
|
5
|
+
readonly offset: number;
|
|
6
|
+
getClientRect(): DOMRect | null;
|
|
7
|
+
}
|
|
3
8
|
declare global {
|
|
4
9
|
interface Document {
|
|
5
|
-
caretPositionFromPoint:
|
|
10
|
+
caretPositionFromPoint(x: number, y: number): CaretPosition | null;
|
|
6
11
|
}
|
|
7
12
|
}
|
|
8
13
|
export declare const useTextSelectionEditor: (text: string, initLeftPos: number, initRightPos: number, leftDrag: boolean, rightDrag: boolean, headClass?: string, selectionClass?: string, tailClass?: string) => [
|
|
@@ -10,3 +15,4 @@ export declare const useTextSelectionEditor: (text: string, initLeftPos: number,
|
|
|
10
15
|
HandlerPos | null,
|
|
11
16
|
HandlerPos | null
|
|
12
17
|
];
|
|
18
|
+
export {};
|