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.
@@ -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: any;
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 != 6)
44
+ if (!node || !node.childNodes || node.childNodes.length !== EXPECTED_CHILD_COUNT)
27
45
  return null;
28
- const headLength = (_c = (_b = (_a = node.childNodes[1].firstChild) === null || _a === void 0 ? void 0 : _a.nodeValue) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
29
- const selLength = (_f = (_e = (_d = node.childNodes[3].firstChild) === null || _d === void 0 ? void 0 : _d.nodeValue) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0;
30
- const tailLength = (_j = (_h = (_g = node.childNodes[5].firstChild) === null || _g === void 0 ? void 0 : _g.nodeValue) === null || _h === void 0 ? void 0 : _h.length) !== null && _j !== void 0 ? _j : 0;
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[3]);
52
+ range.selectNodeContents(node.childNodes[NODE.SEL]);
35
53
  }
36
54
  else if (tailLength > 0) {
37
- range.selectNodeContents(node.childNodes[5]);
55
+ range.selectNodeContents(node.childNodes[NODE.TAIL]);
38
56
  }
39
57
  else {
40
- range.setStart(node.childNodes[1].childNodes[0], headLength);
41
- range.setEnd(node.childNodes[1].childNodes[0], headLength);
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[5]);
72
+ range.selectNodeContents(node.childNodes[NODE.TAIL]);
55
73
  }
56
74
  else if (selLength > 0) {
57
- range.setStart(node.childNodes[3].childNodes[0], selLength);
58
- range.setEnd(node.childNodes[3].childNodes[0], selLength);
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[1].childNodes[0], headLength);
62
- range.setEnd(node.childNodes[1].childNodes[0], headLength);
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[1] && !div.childNodes[1].firstChild) {
75
- div.childNodes[1].appendChild(document.createTextNode(''));
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[3] && !div.childNodes[3].firstChild) {
78
- div.childNodes[3].appendChild(document.createTextNode(''));
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[5] && !div.childNodes[5].firstChild) {
81
- div.childNodes[5].appendChild(document.createTextNode(''));
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
- let range;
86
- let textNode;
87
- let offset;
104
+ var _a, _b;
88
105
  if (document.caretPositionFromPoint) {
89
- range = document.caretPositionFromPoint(x, y);
90
- textNode = range.offsetNode;
91
- offset = range.offset;
92
- }
93
- else if (document.caretRangeFromPoint) {
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 ((textNode === null || textNode === void 0 ? void 0 : textNode.nodeType) === document.TEXT_NODE) {
104
- if (!Number.isNaN(offset)) {
105
- return { node: textNode, offset };
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
- const [currentLeftPos, setCurrentLeftPos] = React.useState(initLeftPos);
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(initRightPos);
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
- }, [textDiv.current]);
124
- // break text into three spans
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, currentLeftPos);
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 < currentRightPos - currentLeftPos)
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, currentRightPos - currentLeftPos);
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
- // mouse move handler
177
- // left handler
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[1].firstChild) {
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[3].firstChild) {
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
- const headText = textDiv.current.childNodes[1].firstChild.nodeValue;
194
- const selText = textDiv.current.childNodes[3].firstChild.nodeValue;
195
- const full = headText + selText;
196
- const nodeChild1 = textDiv.current.childNodes[1].firstChild;
197
- nodeChild1.nodeValue = full.substring(0, posToSet);
198
- const nodeChild3 = textDiv.current.childNodes[3].firstChild;
199
- nodeChild3.nodeValue = full.substring(posToSet);
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, textDiv.current, text]);
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, currentLeftPos, textDiv.current, text]);
214
- React.useLayoutEffect(() => {
215
- setCurrentLeftPos(initLeftPos);
216
- }, [initLeftPos]);
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 === ((_a = textDiv.current) === null || _a === void 0 ? void 0 : _a.childNodes[3].firstChild)) {
269
+ if (sm.node === textDiv.current.childNodes[NODE.SEL].firstChild) {
227
270
  posToSet = currentLeftPos + sm.offset;
228
271
  }
229
- else if (sm.node === ((_b = textDiv.current) === null || _b === void 0 ? void 0 : _b.childNodes[5].firstChild)) {
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
- const selText = textDiv.current.childNodes[3].firstChild.nodeValue;
235
- const tailText = textDiv.current.childNodes[5].firstChild.nodeValue;
236
- const full = selText + tailText;
237
- const nodeChild3 = textDiv.current.childNodes[3].firstChild;
238
- nodeChild3.nodeValue = full.substring(0, posToSet - currentLeftPos);
239
- const nodeChild5 = textDiv.current.childNodes[5].firstChild;
240
- nodeChild5.nodeValue = full.substring(posToSet - currentLeftPos);
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, textDiv.current, text]);
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, currentLeftPos, currentRightPos, textDiv.current, text]);
255
- React.useLayoutEffect(() => {
256
- setCurrentRightPos(initRightPos);
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 === 6) {
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
- setLeftHandler({
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 init right handler
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 === 6) {
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
- setRightHandler({
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
- onChange({
456
+ onChangeRef.current({
385
457
  left: leftHandler.pos,
386
458
  right: rightHandler.pos,
387
459
  });
@@ -0,0 +1,6 @@
1
+ var connect = require('connect');
2
+ var serveStatic = require('serve-static');
3
+
4
+ connect()
5
+ .use(serveStatic(__dirname))
6
+ .listen(3005, () => console.log('Client running on 3005...'));
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 != 6)
24
+ if (!node || !node.childNodes || node.childNodes.length !== EXPECTED_CHILD_COUNT)
7
25
  return null;
8
- const headLength = (_c = (_b = (_a = node.childNodes[1].firstChild) === null || _a === void 0 ? void 0 : _a.nodeValue) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
9
- const selLength = (_f = (_e = (_d = node.childNodes[3].firstChild) === null || _d === void 0 ? void 0 : _d.nodeValue) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0;
10
- const tailLength = (_j = (_h = (_g = node.childNodes[5].firstChild) === null || _g === void 0 ? void 0 : _g.nodeValue) === null || _h === void 0 ? void 0 : _h.length) !== null && _j !== void 0 ? _j : 0;
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[3]);
32
+ range.selectNodeContents(node.childNodes[NODE.SEL]);
15
33
  }
16
34
  else if (tailLength > 0) {
17
- range.selectNodeContents(node.childNodes[5]);
35
+ range.selectNodeContents(node.childNodes[NODE.TAIL]);
18
36
  }
19
37
  else {
20
- range.setStart(node.childNodes[1].childNodes[0], headLength);
21
- range.setEnd(node.childNodes[1].childNodes[0], headLength);
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[5]);
52
+ range.selectNodeContents(node.childNodes[NODE.TAIL]);
35
53
  }
36
54
  else if (selLength > 0) {
37
- range.setStart(node.childNodes[3].childNodes[0], selLength);
38
- range.setEnd(node.childNodes[3].childNodes[0], selLength);
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[1].childNodes[0], headLength);
42
- range.setEnd(node.childNodes[1].childNodes[0], headLength);
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[1] && !div.childNodes[1].firstChild) {
55
- div.childNodes[1].appendChild(document.createTextNode(''));
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[3] && !div.childNodes[3].firstChild) {
58
- div.childNodes[3].appendChild(document.createTextNode(''));
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[5] && !div.childNodes[5].firstChild) {
61
- div.childNodes[5].appendChild(document.createTextNode(''));
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
- let range;
66
- let textNode;
67
- let offset;
84
+ var _a, _b;
68
85
  if (document.caretPositionFromPoint) {
69
- range = document.caretPositionFromPoint(x, y);
70
- textNode = range.offsetNode;
71
- offset = range.offset;
72
- }
73
- else if (document.caretRangeFromPoint) {
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 ((textNode === null || textNode === void 0 ? void 0 : textNode.nodeType) === document.TEXT_NODE) {
84
- if (!Number.isNaN(offset)) {
85
- return { node: textNode, offset };
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
- const [currentLeftPos, setCurrentLeftPos] = useState(initLeftPos);
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(initRightPos);
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
- }, [textDiv.current]);
104
- // break text into three spans
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, currentLeftPos);
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 < currentRightPos - currentLeftPos)
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, currentRightPos - currentLeftPos);
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
- // mouse move handler
157
- // left handler
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[1].firstChild) {
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[3].firstChild) {
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
- const headText = textDiv.current.childNodes[1].firstChild.nodeValue;
174
- const selText = textDiv.current.childNodes[3].firstChild.nodeValue;
175
- const full = headText + selText;
176
- const nodeChild1 = textDiv.current.childNodes[1].firstChild;
177
- nodeChild1.nodeValue = full.substring(0, posToSet);
178
- const nodeChild3 = textDiv.current.childNodes[3].firstChild;
179
- nodeChild3.nodeValue = full.substring(posToSet);
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, textDiv.current, text]);
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, currentLeftPos, textDiv.current, text]);
194
- useLayoutEffect(() => {
195
- setCurrentLeftPos(initLeftPos);
196
- }, [initLeftPos]);
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 === ((_a = textDiv.current) === null || _a === void 0 ? void 0 : _a.childNodes[3].firstChild)) {
249
+ if (sm.node === textDiv.current.childNodes[NODE.SEL].firstChild) {
207
250
  posToSet = currentLeftPos + sm.offset;
208
251
  }
209
- else if (sm.node === ((_b = textDiv.current) === null || _b === void 0 ? void 0 : _b.childNodes[5].firstChild)) {
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
- const selText = textDiv.current.childNodes[3].firstChild.nodeValue;
215
- const tailText = textDiv.current.childNodes[5].firstChild.nodeValue;
216
- const full = selText + tailText;
217
- const nodeChild3 = textDiv.current.childNodes[3].firstChild;
218
- nodeChild3.nodeValue = full.substring(0, posToSet - currentLeftPos);
219
- const nodeChild5 = textDiv.current.childNodes[5].firstChild;
220
- nodeChild5.nodeValue = full.substring(posToSet - currentLeftPos);
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, textDiv.current, text]);
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, currentLeftPos, currentRightPos, textDiv.current, text]);
235
- useLayoutEffect(() => {
236
- setCurrentRightPos(initRightPos);
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 === 6) {
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
- setLeftHandler({
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 init right handler
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 === 6) {
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
- setRightHandler({
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
- onChange({
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: any;
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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-text-range",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "text selection editor for React",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",