From f0bb96220ab2b6f18d71515734bc866375b8b894 Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Sat, 7 Aug 2021 14:39:49 +0800 Subject: [PATCH 01/12] fix: test for ios range line break error --- src/core/features.ts | 47 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/core/features.ts b/src/core/features.ts index 2b5f00126..25d0ccea3 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,49 @@ const testRangeBounds = (document: Document) => { return false; }; +const testIOSLineBreak = (document: Document) => { + if (typeof ''.repeat !== 'function') { + return false; + } + + 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 = '👨'.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'; @@ -128,7 +173,7 @@ export const loadSerializedSVG = (svg: Node): Promise => { export const FEATURES = { get SUPPORT_RANGE_BOUNDS(): boolean { 'use strict'; - const value = testRangeBounds(document); + const value = testRangeBounds(document) && testIOSLineBreak(document); Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value}); return value; }, From c4388844b5d7acc1968010899ad75c6f7a221271 Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Sat, 7 Aug 2021 15:12:09 +0800 Subject: [PATCH 02/12] fix: disable word breaking for broken ios versions --- src/core/features.ts | 8 +++++++- src/css/layout/text.ts | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/features.ts b/src/core/features.ts index 25d0ccea3..2b05b3e79 100644 --- a/src/core/features.ts +++ b/src/core/features.ts @@ -173,10 +173,16 @@ export const loadSerializedSVG = (svg: Node): Promise => { export const FEATURES = { get SUPPORT_RANGE_BOUNDS(): boolean { 'use strict'; - const value = testRangeBounds(document) && testIOSLineBreak(document); + const value = testRangeBounds(document); 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/text.ts b/src/css/layout/text.ts index 247392663..a53d3c79b 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -73,7 +73,9 @@ const getRangeBounds = (context: Context, node: Text, offset: number, length: nu }; const breakText = (value: string, styles: CSSParsedDeclaration): string[] => { - return styles.letterSpacing !== 0 ? toCodePoints(value).map((i) => fromCodePoint(i)) : breakWords(value, styles); + return styles.letterSpacing !== 0 || !FEATURES.SUPPORT_WORD_BREAKING + ? toCodePoints(value).map((i) => fromCodePoint(i)) + : breakWords(value, styles); }; // https://drafts.csswg.org/css-text/#word-separator From 5e1a17a7531e6b85b7981fd0b16d0d7e6eab5b7b Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Sat, 7 Aug 2021 16:45:51 +0800 Subject: [PATCH 03/12] fix: split text for ranges --- src/core/features.ts | 6 +----- src/css/layout/text.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/core/features.ts b/src/core/features.ts index 2b05b3e79..78514bdd9 100644 --- a/src/core/features.ts +++ b/src/core/features.ts @@ -25,10 +25,6 @@ const testRangeBounds = (document: Document) => { }; const testIOSLineBreak = (document: Document) => { - if (typeof ''.repeat !== 'function') { - return false; - } - const testElement = document.createElement('boundtest'); testElement.style.width = '50px'; testElement.style.display = 'block'; @@ -38,7 +34,7 @@ const testIOSLineBreak = (document: Document) => { document.body.appendChild(testElement); const range = document.createRange(); - testElement.innerHTML = '👨'.repeat(10); + testElement.innerHTML = typeof ''.repeat === 'function' ? '👨'.repeat(10) : ''; const node = testElement.firstChild as Text; diff --git a/src/css/layout/text.ts b/src/css/layout/text.ts index a53d3c79b..71206b370 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -27,7 +27,13 @@ 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) { + const replacementNode = node.splitText(text.length); + textBounds.push(new TextBounds(text, getRangeBounds(context, node, 0, text.length))); + node = replacementNode; + } 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))); @@ -73,9 +79,7 @@ const getRangeBounds = (context: Context, node: Text, offset: number, length: nu }; const breakText = (value: string, styles: CSSParsedDeclaration): string[] => { - return styles.letterSpacing !== 0 || !FEATURES.SUPPORT_WORD_BREAKING - ? toCodePoints(value).map((i) => fromCodePoint(i)) - : breakWords(value, styles); + return styles.letterSpacing !== 0 ? toCodePoints(value).map((i) => fromCodePoint(i)) : breakWords(value, styles); }; // https://drafts.csswg.org/css-text/#word-separator From da4cf33c07915e577de4bb4e0396ac2d879c4af4 Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Sat, 7 Aug 2021 18:06:54 +0800 Subject: [PATCH 04/12] try getClientRects --- src/css/layout/text.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/css/layout/text.ts b/src/css/layout/text.ts index 71206b370..d33cb4fb7 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -75,7 +75,7 @@ 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 Bounds.fromClientRect(context, range.getClientRects()[0]); }; const breakText = (value: string, styles: CSSParsedDeclaration): string[] => { From 37cf62216fdb634b953e50d1d62c440921c07b24 Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Mon, 9 Aug 2021 12:44:53 +0800 Subject: [PATCH 05/12] try getClientRect --- src/css/layout/bounds.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/css/layout/bounds.ts b/src/css/layout/bounds.ts index 04596e305..a22df559b 100644 --- a/src/css/layout/bounds.ts +++ b/src/css/layout/bounds.ts @@ -8,12 +8,14 @@ export class Bounds { } static fromClientRect(context: Context, clientRect: ClientRect): Bounds { - return new Bounds( - clientRect.left + context.windowBounds.left, - clientRect.top + context.windowBounds.top, - clientRect.width, - clientRect.height - ); + return clientRect + ? new Bounds( + clientRect.left + context.windowBounds.left, + clientRect.top + context.windowBounds.top, + clientRect.width, + clientRect.height + ) + : new Bounds(0, 0, 0, 0); } } From 771e523f3f922db5a8343ea304c6f06c7ce78a9c Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Mon, 9 Aug 2021 13:12:07 +0800 Subject: [PATCH 06/12] Revert "try getClientRect" This reverts commit 37cf62216fdb634b953e50d1d62c440921c07b24. --- src/css/layout/bounds.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/css/layout/bounds.ts b/src/css/layout/bounds.ts index a22df559b..04596e305 100644 --- a/src/css/layout/bounds.ts +++ b/src/css/layout/bounds.ts @@ -8,14 +8,12 @@ export class Bounds { } static fromClientRect(context: Context, clientRect: ClientRect): Bounds { - return clientRect - ? new Bounds( - clientRect.left + context.windowBounds.left, - clientRect.top + context.windowBounds.top, - clientRect.width, - clientRect.height - ) - : new Bounds(0, 0, 0, 0); + return new Bounds( + clientRect.left + context.windowBounds.left, + clientRect.top + context.windowBounds.top, + clientRect.width, + clientRect.height + ); } } From 300db8f9c873f66f2d0d6e56772489bc903f6be8 Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Mon, 9 Aug 2021 13:12:15 +0800 Subject: [PATCH 07/12] Revert "try getClientRects" This reverts commit da4cf33c07915e577de4bb4e0396ac2d879c4af4. --- src/css/layout/text.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/css/layout/text.ts b/src/css/layout/text.ts index d33cb4fb7..71206b370 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -75,7 +75,7 @@ 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.getClientRects()[0]); + return Bounds.fromClientRect(context, range.getBoundingClientRect()); }; const breakText = (value: string, styles: CSSParsedDeclaration): string[] => { From 29663836668f24c38109bddda1e922d92b4fbf99 Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Mon, 9 Aug 2021 13:13:45 +0800 Subject: [PATCH 08/12] try range length 1 --- src/css/layout/text.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/css/layout/text.ts b/src/css/layout/text.ts index 71206b370..c47ae36ba 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -28,9 +28,7 @@ export const parseTextBounds = ( if (styles.textDecorationLine.length || text.trim().length > 0) { if (FEATURES.SUPPORT_RANGE_BOUNDS) { if (!FEATURES.SUPPORT_WORD_BREAKING) { - const replacementNode = node.splitText(text.length); - textBounds.push(new TextBounds(text, getRangeBounds(context, node, 0, text.length))); - node = replacementNode; + textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, 1))); } else { textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length))); } From 68b26a09e740f71e13f64d2db6e0eee10922719d Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Mon, 9 Aug 2021 15:10:23 +0800 Subject: [PATCH 09/12] Revert "try range length 1" This reverts commit 29663836668f24c38109bddda1e922d92b4fbf99. --- src/css/layout/text.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/css/layout/text.ts b/src/css/layout/text.ts index c47ae36ba..71206b370 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -28,7 +28,9 @@ export const parseTextBounds = ( if (styles.textDecorationLine.length || text.trim().length > 0) { if (FEATURES.SUPPORT_RANGE_BOUNDS) { if (!FEATURES.SUPPORT_WORD_BREAKING) { - textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, 1))); + const replacementNode = node.splitText(text.length); + textBounds.push(new TextBounds(text, getRangeBounds(context, node, 0, text.length))); + node = replacementNode; } else { textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length))); } From 6f1dc8ab734283f449387975cee059aa77b55c6f Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Mon, 9 Aug 2021 15:17:25 +0800 Subject: [PATCH 10/12] try with getClientRects --- src/css/layout/bounds.ts | 2 ++ src/css/layout/text.ts | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/css/layout/bounds.ts b/src/css/layout/bounds.ts index 04596e305..f17f9ee75 100644 --- a/src/css/layout/bounds.ts +++ b/src/css/layout/bounds.ts @@ -15,6 +15,8 @@ export class Bounds { clientRect.height ); } + + 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 71206b370..a76b797c1 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -28,9 +28,9 @@ export const parseTextBounds = ( if (styles.textDecorationLine.length || text.trim().length > 0) { if (FEATURES.SUPPORT_RANGE_BOUNDS) { if (!FEATURES.SUPPORT_WORD_BREAKING) { - const replacementNode = node.splitText(text.length); - textBounds.push(new TextBounds(text, getRangeBounds(context, node, 0, text.length))); - node = replacementNode; + const range = createRange(node, offset, text.length); + const domRect: DOMRect = range.getClientRects()[0]; + return domRect ? Bounds.fromClientRect(context, domRect) : Bounds.empty; } else { textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length))); } @@ -64,10 +64,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'); @@ -75,7 +75,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[] => { From e82bad2a68b0e31b2584f8733d2208ace27f67da Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Mon, 9 Aug 2021 16:36:03 +0800 Subject: [PATCH 11/12] from domrectlist --- src/css/layout/bounds.ts | 14 +++++++++++++- src/css/layout/text.ts | 10 +++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/css/layout/bounds.ts b/src/css/layout/bounds.ts index f17f9ee75..43d1bea1c 100644 --- a/src/css/layout/bounds.ts +++ b/src/css/layout/bounds.ts @@ -16,7 +16,19 @@ export class Bounds { ); } - static empty = new Bounds(0, 0, 0, 0); + 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 a76b797c1..5ba4a0b2b 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -29,8 +29,12 @@ export const parseTextBounds = ( if (FEATURES.SUPPORT_RANGE_BOUNDS) { if (!FEATURES.SUPPORT_WORD_BREAKING) { const range = createRange(node, offset, text.length); - const domRect: DOMRect = range.getClientRects()[0]; - return domRect ? Bounds.fromClientRect(context, domRect) : Bounds.empty; + 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))); } @@ -64,7 +68,7 @@ const getWrapperBounds = (context: Context, node: Text): Bounds => { } } - return Bounds.empty; + return Bounds.EMPTY; }; const createRange = (node: Text, offset: number, length: number): Range => { From 01c5e6df36a0e9bc1bbc5490a2e894fdd21dffb2 Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Mon, 9 Aug 2021 16:40:06 +0800 Subject: [PATCH 12/12] fix lint --- src/css/layout/text.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/css/layout/text.ts b/src/css/layout/text.ts index 5ba4a0b2b..9b33e7c1a 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -28,7 +28,6 @@ export const parseTextBounds = ( if (styles.textDecorationLine.length || text.trim().length > 0) { if (FEATURES.SUPPORT_RANGE_BOUNDS) { if (!FEATURES.SUPPORT_WORD_BREAKING) { - const range = createRange(node, offset, text.length); textBounds.push( new TextBounds( text,