Skip to content

Commit 8de3cf0

Browse files
author
Mingze
authored
fix(range): Fix Range.getClientRects on IE/Edge (#577)
* fix(range): Fix Range.getClientRects on IE/Edge * feat(region): Move test to actions-test
1 parent 53b00bd commit 8de3cf0

File tree

3 files changed

+111
-1
lines changed

3 files changed

+111
-1
lines changed

src/store/highlight/__mocks__/data.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ export const mockDOMRect: DOMRect = {
2222
y: 200,
2323
};
2424

25+
const mockTextNode = document.createTextNode('test');
26+
2527
export const mockRange: Range = ({
28+
endContainer: mockTextNode,
2629
getBoundingClientRect: () => mockDOMRect,
2730
getClientRects: () => [mockDOMRect],
31+
startContainer: mockTextNode,
2832
} as unknown) as Range;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import state from '../__mocks__/highlightState';
2+
import { mockContainerRect, mockDOMRect, mockRange } from '../__mocks__/data';
3+
import { setSelectionAction } from '../actions';
4+
5+
describe('store/highlight/actions', () => {
6+
describe('setSelectionAction', () => {
7+
const arg = {
8+
containerRect: mockContainerRect,
9+
location: 1,
10+
range: mockRange,
11+
};
12+
13+
test('should prepare correct argument', () => {
14+
expect(setSelectionAction(arg)).toEqual({
15+
payload: state.selection,
16+
type: 'SET_SELECTION',
17+
});
18+
});
19+
20+
test('should prepare correct argument in IE/Edge', () => {
21+
jest.spyOn(document, 'createNodeIterator').mockReturnValueOnce(({
22+
nextNode: jest.fn().mockReturnValueOnce(mockRange.startContainer),
23+
} as unknown) as NodeIterator);
24+
jest.spyOn(document, 'createRange').mockReturnValueOnce({
25+
...new Range(),
26+
getBoundingClientRect: jest.fn().mockReturnValueOnce(mockDOMRect),
27+
selectNodeContents: jest.fn(),
28+
setEnd: jest.fn(),
29+
setStart: jest.fn(),
30+
});
31+
const range = {
32+
...mockRange,
33+
getClientRects: () => ({ length: 0 } as DOMRectList),
34+
};
35+
36+
const newArg = {
37+
...arg,
38+
range,
39+
};
40+
41+
expect(setSelectionAction(newArg)).toEqual({
42+
payload: state.selection,
43+
type: 'SET_SELECTION',
44+
});
45+
});
46+
});
47+
});

src/store/highlight/actions.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,59 @@ type Payload = {
1313
payload: SelectionItem | null;
1414
};
1515

16+
declare global {
17+
interface Document {
18+
createNodeIterator(
19+
root: Node,
20+
whatToShow?: number,
21+
filter?: NodeFilter | Function,
22+
entityReferenceExpansion?: boolean,
23+
): NodeIterator;
24+
}
25+
}
26+
27+
export const getClientRects = (range: Range): DOMRect[] => {
28+
const iterator = document.createNodeIterator(
29+
range.commonAncestorContainer,
30+
NodeFilter.SHOW_ALL,
31+
function acceptNode() {
32+
return NodeFilter.FILTER_ACCEPT;
33+
},
34+
false,
35+
);
36+
37+
const newRange = document.createRange();
38+
const rects = [];
39+
let currentNode = iterator.nextNode();
40+
while (currentNode) {
41+
if (rects.length === 0 && currentNode !== range.startContainer) {
42+
currentNode = iterator.nextNode();
43+
// eslint-disable-next-line no-continue
44+
continue;
45+
}
46+
47+
// Only highlight Text nodes
48+
if (currentNode.nodeType === Node.TEXT_NODE) {
49+
newRange.selectNodeContents(currentNode);
50+
if (currentNode === range.startContainer) {
51+
newRange.setStart(currentNode, range.startOffset);
52+
}
53+
if (currentNode === range.endContainer) {
54+
newRange.setEnd(currentNode, range.endOffset);
55+
}
56+
rects.push(newRange.getBoundingClientRect());
57+
}
58+
59+
if (currentNode === range.endContainer) {
60+
break;
61+
}
62+
63+
currentNode = iterator.nextNode();
64+
}
65+
66+
return rects;
67+
};
68+
1669
export const getShape = ({ height, left, top, width }: DOMRect): Shape => ({
1770
height,
1871
width,
@@ -31,11 +84,17 @@ export const setSelectionAction = createAction(
3184

3285
const { containerRect, location, range } = arg;
3386

87+
let rects = Array.from(range.getClientRects());
88+
// getClientRects on IE/Edge might return 0 rects
89+
if (!rects.length) {
90+
rects = getClientRects(range);
91+
}
92+
3493
return {
3594
payload: {
3695
containerRect: getShape(containerRect),
3796
location,
38-
rects: combineRectsByRow(Array.from(range.getClientRects()).map(getShape)),
97+
rects: combineRectsByRow(rects.map(getShape)),
3998
},
4099
};
41100
},

0 commit comments

Comments
 (0)