Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: test for ios range line break error #2635

Merged
merged 12 commits into from
Aug 9, 2021
47 changes: 47 additions & 0 deletions src/core/features.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {fromCodePoint, toCodePoints} from 'css-line-break';

const testRangeBounds = (document: Document) => {
const TEST_HEIGHT = 123;

Expand All @@ -22,6 +24,45 @@ const testRangeBounds = (document: Document) => {
return false;
};

const testIOSLineBreak = (document: Document) => {
const testElement = document.createElement('boundtest');
testElement.style.width = '50px';
testElement.style.display = 'block';
testElement.style.fontSize = '12px';
testElement.style.letterSpacing = '0px';
testElement.style.wordSpacing = '0px';
document.body.appendChild(testElement);
const range = document.createRange();

testElement.innerHTML = typeof ''.repeat === 'function' ? '👨'.repeat(10) : '';

const node = testElement.firstChild as Text;

const textList = toCodePoints(node.data).map((i) => fromCodePoint(i));
let offset = 0;
let prev: DOMRect = {} as DOMRect;

// ios 13 does not handle range getBoundingClientRect line changes correctly #2177
const supports = textList.every((text, i) => {
range.setStart(node, offset);
range.setEnd(node, offset + text.length);
const rect = range.getBoundingClientRect();

offset += text.length;
const boundAhead = rect.x > prev.x || rect.y > prev.y;

prev = rect;
if (i === 0) {
return true;
}

return boundAhead;
});

document.body.removeChild(testElement);
return supports;
};

const testCORS = (): boolean => typeof new Image().crossOrigin !== 'undefined';

const testResponseType = (): boolean => typeof new XMLHttpRequest().responseType === 'string';
Expand Down Expand Up @@ -132,6 +173,12 @@ export const FEATURES = {
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
return value;
},
get SUPPORT_WORD_BREAKING(): boolean {
'use strict';
const value = FEATURES.SUPPORT_RANGE_BOUNDS && testIOSLineBreak(document);
Object.defineProperty(FEATURES, 'SUPPORT_WORD_BREAKING', {value});
return value;
},
get SUPPORT_SVG_DRAWING(): boolean {
'use strict';
const value = testSVG(document);
Expand Down
14 changes: 14 additions & 0 deletions src/css/layout/bounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ export class Bounds {
clientRect.height
);
}

static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds {
const domRect = domRectList[0];
return domRect
? new Bounds(
domRect.x + context.windowBounds.left,
domRect.y + context.windowBounds.top,
domRect.width,
domRect.height
)
: Bounds.EMPTY;
}

static EMPTY = new Bounds(0, 0, 0, 0);
}

export const parseBounds = (context: Context, node: Element): Bounds => {
Expand Down
21 changes: 17 additions & 4 deletions src/css/layout/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@ export const parseTextBounds = (
textList.forEach((text) => {
if (styles.textDecorationLine.length || text.trim().length > 0) {
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length)));
if (!FEATURES.SUPPORT_WORD_BREAKING) {
textBounds.push(
new TextBounds(
text,
Bounds.fromDOMRectList(context, createRange(node, offset, text.length).getClientRects())
)
);
} else {
textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length)));
}
} else {
const replacementNode = node.splitText(text.length);
textBounds.push(new TextBounds(text, getWrapperBounds(context, node)));
Expand Down Expand Up @@ -58,18 +67,22 @@ const getWrapperBounds = (context: Context, node: Text): Bounds => {
}
}

return new Bounds(0, 0, 0, 0);
return Bounds.EMPTY;
};

const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => {
const createRange = (node: Text, offset: number, length: number): Range => {
const ownerDocument = node.ownerDocument;
if (!ownerDocument) {
throw new Error('Node has no owner document');
}
const range = ownerDocument.createRange();
range.setStart(node, offset);
range.setEnd(node, offset + length);
return Bounds.fromClientRect(context, range.getBoundingClientRect());
return range;
};

const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => {
return Bounds.fromClientRect(context, createRange(node, offset, length).getBoundingClientRect());
};

const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
Expand Down