From f43f942fcd793dde9cdc6c0438f379ec3c05c405 Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Mon, 9 Aug 2021 17:14:40 +0800 Subject: [PATCH] fix: test for ios range line break error (#2635) --- src/core/features.ts | 47 ++++++++++++++++++++++++++++++++++++++++ src/css/layout/bounds.ts | 14 ++++++++++++ src/css/layout/text.ts | 21 ++++++++++++++---- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/core/features.ts b/src/core/features.ts index 2b5f001..78514bd 100644 --- a/src/core/features.ts +++ b/src/core/features.ts @@ -1,3 +1,5 @@ +import {fromCodePoint, toCodePoints} from 'css-line-break'; + const testRangeBounds = (document: Document) => { const TEST_HEIGHT = 123; @@ -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'; @@ -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); diff --git a/src/css/layout/bounds.ts b/src/css/layout/bounds.ts index 04596e3..43d1bea 100644 --- a/src/css/layout/bounds.ts +++ b/src/css/layout/bounds.ts @@ -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 => { diff --git a/src/css/layout/text.ts b/src/css/layout/text.ts index 2473926..9b33e7c 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -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))); @@ -58,10 +67,10 @@ 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'); @@ -69,7 +78,11 @@ const getRangeBounds = (context: Context, node: Text, offset: number, length: nu 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[] => {