TextSource* API updates (#2236)
* Move TextSourceRange static functions to DocumentUtil getWritingMode is also simplified * Update Google Docs range to be empty to match other range sources * Rename imposterContainer to imposterElement * Add static creation functions * Add static creation function * Remove unused collapse function * Don't select imposter elements * Refactor setEndOffset * Adjust argument order for setEndOffset * Update TextSourceRange constructor * Remove unused isConnected * Cache rects * Fix test * Remove unused getRect * Revert "Fix test" * Remove cachedRect * Use the source element rect to handle scroll differences * Writing mode update * Remove _cachedRects update This shouldn't be necessary as the imposter is usually detached almost immediately after scanning, giving no time for the window to be resized or scrolled.
This commit is contained in:
parent
8240482e9d
commit
75d3059451
@ -87,7 +87,7 @@ class GoogleDocsUtil {
|
|||||||
const range = this._getRangeWithPoint(content, x, y, normalizeCssZoom);
|
const range = this._getRangeWithPoint(content, x, y, normalizeCssZoom);
|
||||||
this._setImportantStyle(textStyle, 'pointer-events', 'none');
|
this._setImportantStyle(textStyle, 'pointer-events', 'none');
|
||||||
this._setImportantStyle(textStyle, 'opacity', '0');
|
this._setImportantStyle(textStyle, 'opacity', '0');
|
||||||
return new TextSourceRange(range, '', svgText, element);
|
return TextSourceRange.createFromImposter(range, svgText, element);
|
||||||
}
|
}
|
||||||
|
|
||||||
static _getRangeWithPoint(textNode, x, y, normalizeCssZoom) {
|
static _getRangeWithPoint(textNode, x, y, normalizeCssZoom) {
|
||||||
@ -110,7 +110,7 @@ class GoogleDocsUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
range.setStart(textNode, start);
|
range.setStart(textNode, start);
|
||||||
range.setEnd(textNode, end);
|
range.setEnd(textNode, start);
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -757,7 +757,7 @@ class Frontend {
|
|||||||
async _scanSelectedText(allowEmptyRange) {
|
async _scanSelectedText(allowEmptyRange) {
|
||||||
const range = this._getFirstSelectionRange(allowEmptyRange);
|
const range = this._getFirstSelectionRange(allowEmptyRange);
|
||||||
if (range === null) { return false; }
|
if (range === null) { return false; }
|
||||||
const source = new TextSourceRange(range, range.toString(), null, null);
|
const source = TextSourceRange.create(range);
|
||||||
await this._textScanner.search(source, {focus: true, restoreSelection: true});
|
await this._textScanner.search(source, {focus: true, restoreSelection: true});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ class DocumentUtil {
|
|||||||
case 'IMG':
|
case 'IMG':
|
||||||
case 'BUTTON':
|
case 'BUTTON':
|
||||||
case 'SELECT':
|
case 'SELECT':
|
||||||
return new TextSourceElement(element);
|
return TextSourceElement.create(element);
|
||||||
case 'INPUT':
|
case 'INPUT':
|
||||||
if (element.type === 'text') {
|
if (element.type === 'text') {
|
||||||
imposterSourceElement = element;
|
imposterSourceElement = element;
|
||||||
@ -85,8 +85,9 @@ class DocumentUtil {
|
|||||||
if (imposter !== null) {
|
if (imposter !== null) {
|
||||||
this._setImposterStyle(imposterContainer.style, 'z-index', '-2147483646');
|
this._setImposterStyle(imposterContainer.style, 'z-index', '-2147483646');
|
||||||
this._setImposterStyle(imposter.style, 'pointer-events', 'none');
|
this._setImposterStyle(imposter.style, 'pointer-events', 'none');
|
||||||
|
return TextSourceRange.createFromImposter(range, imposterContainer, imposterSourceElement);
|
||||||
}
|
}
|
||||||
return new TextSourceRange(range, '', imposterContainer, imposterSourceElement);
|
return TextSourceRange.create(range);
|
||||||
} else {
|
} else {
|
||||||
if (imposterContainer !== null) {
|
if (imposterContainer !== null) {
|
||||||
imposterContainer.parentNode.removeChild(imposterContainer);
|
imposterContainer.parentNode.removeChild(imposterContainer);
|
||||||
@ -131,7 +132,7 @@ class DocumentUtil {
|
|||||||
// Scan text
|
// Scan text
|
||||||
source = source.clone();
|
source = source.clone();
|
||||||
const startLength = source.setStartOffset(extent, layoutAwareScan);
|
const startLength = source.setStartOffset(extent, layoutAwareScan);
|
||||||
const endLength = source.setEndOffset(extent * 2 - startLength, layoutAwareScan, true);
|
const endLength = source.setEndOffset(extent * 2 - startLength, true, layoutAwareScan);
|
||||||
const text = source.text();
|
const text = source.text();
|
||||||
const textLength = text.length;
|
const textLength = text.length;
|
||||||
const textEndAnchor = textLength - endLength;
|
const textEndAnchor = textLength - endLength;
|
||||||
@ -418,6 +419,58 @@ class DocumentUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offsets an array of DOMRects by a given amount.
|
||||||
|
* @param {DOMRect[]} rects The DOMRects to offset.
|
||||||
|
* @param {number} x The horizontal offset amount.
|
||||||
|
* @param {number} y The vertical offset amount.
|
||||||
|
* @returns {DOMRect} The DOMRects with the offset applied.
|
||||||
|
*/
|
||||||
|
static offsetDOMRects(rects, x, y) {
|
||||||
|
const results = [];
|
||||||
|
for (const rect of rects) {
|
||||||
|
results.push(new DOMRect(rect.left + x, rect.top + y, rect.width, rect.height));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the parent writing mode of an element.
|
||||||
|
* See: https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode.
|
||||||
|
* @param {Element} element The HTML element to check.
|
||||||
|
* @returns {string} The writing mode.
|
||||||
|
*/
|
||||||
|
static getElementWritingMode(element) {
|
||||||
|
if (element !== null) {
|
||||||
|
const {writingMode} = getComputedStyle(element);
|
||||||
|
if (typeof writingMode === 'string') {
|
||||||
|
return this.normalizeWritingMode(writingMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'horizontal-tb';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a CSS writing mode value by converting non-standard and deprecated values
|
||||||
|
* into their corresponding standard vaules.
|
||||||
|
* @param {string} writingMode The writing mode to normalize.
|
||||||
|
* @returns {string} The normalized writing mode.
|
||||||
|
*/
|
||||||
|
static normalizeWritingMode(writingMode) {
|
||||||
|
switch (writingMode) {
|
||||||
|
case 'lr':
|
||||||
|
case 'lr-tb':
|
||||||
|
case 'rl':
|
||||||
|
return 'horizontal-tb';
|
||||||
|
case 'tb':
|
||||||
|
return 'vertical-lr';
|
||||||
|
case 'tb-rl':
|
||||||
|
return 'vertical-rl';
|
||||||
|
default:
|
||||||
|
return writingMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
static _getActiveButtons(event, array) {
|
static _getActiveButtons(event, array) {
|
||||||
|
@ -21,9 +21,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class TextSourceElement {
|
class TextSourceElement {
|
||||||
constructor(element, fullContent=null, startOffset=0, endOffset=0) {
|
constructor(element, fullContent, startOffset, endOffset) {
|
||||||
this._element = element;
|
this._element = element;
|
||||||
this._fullContent = (typeof fullContent === 'string' ? fullContent : TextSourceElement.getElementContent(element));
|
this._fullContent = fullContent;
|
||||||
this._startOffset = startOffset;
|
this._startOffset = startOffset;
|
||||||
this._endOffset = endOffset;
|
this._endOffset = endOffset;
|
||||||
this._content = this._fullContent.substring(this._startOffset, this._endOffset);
|
this._content = this._fullContent.substring(this._startOffset, this._endOffset);
|
||||||
@ -49,10 +49,6 @@ class TextSourceElement {
|
|||||||
return this._endOffset;
|
return this._endOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isConnected() {
|
|
||||||
return this._element.isConnected;
|
|
||||||
}
|
|
||||||
|
|
||||||
clone() {
|
clone() {
|
||||||
return new TextSourceElement(this._element, this._fullContent, this._startOffset, this._endOffset);
|
return new TextSourceElement(this._element, this._fullContent, this._startOffset, this._endOffset);
|
||||||
}
|
}
|
||||||
@ -65,7 +61,7 @@ class TextSourceElement {
|
|||||||
return this._content;
|
return this._content;
|
||||||
}
|
}
|
||||||
|
|
||||||
setEndOffset(length, _layoutAwareScan, fromEnd) {
|
setEndOffset(length, fromEnd) {
|
||||||
const offset = fromEnd ? this._endOffset : this._startOffset;
|
const offset = fromEnd ? this._endOffset : this._startOffset;
|
||||||
length = Math.min(this._fullContent.length - offset, length);
|
length = Math.min(this._fullContent.length - offset, length);
|
||||||
if (length > 0) {
|
if (length > 0) {
|
||||||
@ -86,19 +82,6 @@ class TextSourceElement {
|
|||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
collapse(toStart) {
|
|
||||||
if (toStart) {
|
|
||||||
this._endOffset = this._startOffset;
|
|
||||||
} else {
|
|
||||||
this._startOffset = this._endOffset;
|
|
||||||
}
|
|
||||||
this._content = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
getRect() {
|
|
||||||
return DocumentUtil.convertRectZoomCoordinates(this._element.getBoundingClientRect(), this._element);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRects() {
|
getRects() {
|
||||||
return DocumentUtil.convertMultipleRectZoomCoordinates(this._element.getClientRects(), this._element);
|
return DocumentUtil.convertMultipleRectZoomCoordinates(this._element.getClientRects(), this._element);
|
||||||
}
|
}
|
||||||
@ -130,7 +113,11 @@ class TextSourceElement {
|
|||||||
return [this._element];
|
return [this._element];
|
||||||
}
|
}
|
||||||
|
|
||||||
static getElementContent(element) {
|
static create(element) {
|
||||||
|
return new TextSourceElement(element, this._getElementContent(element), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _getElementContent(element) {
|
||||||
let content;
|
let content;
|
||||||
switch (element.nodeName.toUpperCase()) {
|
switch (element.nodeName.toUpperCase()) {
|
||||||
case 'BUTTON':
|
case 'BUTTON':
|
||||||
|
@ -21,12 +21,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class TextSourceRange {
|
class TextSourceRange {
|
||||||
constructor(range, content, imposterContainer, imposterSourceElement) {
|
constructor(range, rangeStartOffset, content, imposterElement, imposterSourceElement, cachedRects, cachedSourceRect) {
|
||||||
this._range = range;
|
this._range = range;
|
||||||
this._rangeStartOffset = range.startOffset;
|
this._rangeStartOffset = rangeStartOffset;
|
||||||
this._content = content;
|
this._content = content;
|
||||||
this._imposterContainer = imposterContainer;
|
this._imposterElement = imposterElement;
|
||||||
this._imposterSourceElement = imposterSourceElement;
|
this._imposterSourceElement = imposterSourceElement;
|
||||||
|
this._cachedRects = cachedRects;
|
||||||
|
this._cachedSourceRect = cachedSourceRect;
|
||||||
}
|
}
|
||||||
|
|
||||||
get type() {
|
get type() {
|
||||||
@ -45,20 +47,21 @@ class TextSourceRange {
|
|||||||
return this._imposterSourceElement;
|
return this._imposterSourceElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isConnected() {
|
clone() {
|
||||||
return (
|
return new TextSourceRange(
|
||||||
this._range.startContainer.isConnected &&
|
this._range.cloneRange(),
|
||||||
this._range.endContainer.isConnected
|
this._rangeStartOffset,
|
||||||
|
this._content,
|
||||||
|
this._imposterElement,
|
||||||
|
this._imposterSourceElement,
|
||||||
|
this._cachedRects,
|
||||||
|
this._cachedSourceRect
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
clone() {
|
|
||||||
return new TextSourceRange(this._range.cloneRange(), this._content, this._imposterContainer, this._imposterSourceElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if (this._imposterContainer !== null && this._imposterContainer.parentNode !== null) {
|
if (this._imposterElement !== null && this._imposterElement.parentNode !== null) {
|
||||||
this._imposterContainer.parentNode.removeChild(this._imposterContainer);
|
this._imposterElement.parentNode.removeChild(this._imposterElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,12 +69,17 @@ class TextSourceRange {
|
|||||||
return this._content;
|
return this._content;
|
||||||
}
|
}
|
||||||
|
|
||||||
setEndOffset(length, layoutAwareScan, fromEnd) {
|
setEndOffset(length, fromEnd, layoutAwareScan) {
|
||||||
const state = (
|
let node;
|
||||||
fromEnd ?
|
let offset;
|
||||||
new DOMTextScanner(this._range.endContainer, this._range.endOffset, !layoutAwareScan, layoutAwareScan).seek(length) :
|
if (fromEnd) {
|
||||||
new DOMTextScanner(this._range.startContainer, this._range.startOffset, !layoutAwareScan, layoutAwareScan).seek(length)
|
node = this._range.endContainer;
|
||||||
);
|
offset = this._range.endOffset;
|
||||||
|
} else {
|
||||||
|
node = this._range.startContainer;
|
||||||
|
offset = this._range.startOffset;
|
||||||
|
}
|
||||||
|
const state = new DOMTextScanner(node, offset, !layoutAwareScan, layoutAwareScan).seek(length);
|
||||||
this._range.setEnd(state.node, state.offset);
|
this._range.setEnd(state.node, state.offset);
|
||||||
this._content = (fromEnd ? this._content + state.content : state.content);
|
this._content = (fromEnd ? this._content + state.content : state.content);
|
||||||
return length - state.remainder;
|
return length - state.remainder;
|
||||||
@ -85,30 +93,25 @@ class TextSourceRange {
|
|||||||
return length - state.remainder;
|
return length - state.remainder;
|
||||||
}
|
}
|
||||||
|
|
||||||
collapse(toStart) {
|
|
||||||
this._range.collapse(toStart);
|
|
||||||
this._content = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
getRect() {
|
|
||||||
return DocumentUtil.convertRectZoomCoordinates(this._range.getBoundingClientRect(), this._range.startContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRects() {
|
getRects() {
|
||||||
|
if (this._isImposterDisconnected()) { return this._getCachedRects(); }
|
||||||
return DocumentUtil.convertMultipleRectZoomCoordinates(this._range.getClientRects(), this._range.startContainer);
|
return DocumentUtil.convertMultipleRectZoomCoordinates(this._range.getClientRects(), this._range.startContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
getWritingMode() {
|
getWritingMode() {
|
||||||
return TextSourceRange.getElementWritingMode(TextSourceRange.getParentElement(this._range.startContainer));
|
const node = this._isImposterDisconnected() ? this._imposterSourceElement : this._range.startContainer;
|
||||||
|
return DocumentUtil.getElementWritingMode(node !== null ? node.parentElement : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
select() {
|
select() {
|
||||||
|
if (this._imposterElement !== null) { return; }
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(this._range);
|
selection.addRange(this._range);
|
||||||
}
|
}
|
||||||
|
|
||||||
deselect() {
|
deselect() {
|
||||||
|
if (this._imposterElement !== null) { return; }
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
}
|
}
|
||||||
@ -143,36 +146,26 @@ class TextSourceRange {
|
|||||||
return DocumentUtil.getNodesInRange(this._range);
|
return DocumentUtil.getNodesInRange(this._range);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getParentElement(node) {
|
static create(range) {
|
||||||
while (node !== null && node.nodeType !== Node.ELEMENT_NODE) {
|
return new TextSourceRange(range, range.startOffset, range.toString(), null, null, null, null);
|
||||||
node = node.parentNode;
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getElementWritingMode(element) {
|
static createFromImposter(range, imposterElement, imposterSourceElement) {
|
||||||
if (element !== null) {
|
const cachedRects = DocumentUtil.convertMultipleRectZoomCoordinates(range.getClientRects(), range.startContainer);
|
||||||
const style = window.getComputedStyle(element);
|
const cachedSourceRect = DocumentUtil.convertRectZoomCoordinates(imposterSourceElement.getBoundingClientRect(), imposterSourceElement);
|
||||||
const writingMode = style.writingMode;
|
return new TextSourceRange(range, range.startOffset, range.toString(), imposterElement, imposterSourceElement, cachedRects, cachedSourceRect);
|
||||||
if (typeof writingMode === 'string') {
|
|
||||||
return TextSourceRange.normalizeWritingMode(writingMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'horizontal-tb';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static normalizeWritingMode(writingMode) {
|
_isImposterDisconnected() {
|
||||||
switch (writingMode) {
|
return this._imposterElement !== null && !this._imposterElement.isConnected;
|
||||||
case 'lr':
|
}
|
||||||
case 'lr-tb':
|
|
||||||
case 'rl':
|
_getCachedRects() {
|
||||||
return 'horizontal-tb';
|
const sourceRect = DocumentUtil.convertRectZoomCoordinates(this._imposterSourceElement.getBoundingClientRect(), this._imposterSourceElement);
|
||||||
case 'tb':
|
return DocumentUtil.offsetDOMRects(
|
||||||
return 'vertical-lr';
|
this._cachedRects,
|
||||||
case 'tb-rl':
|
sourceRect.left - this._cachedSourceRect.left,
|
||||||
return 'vertical-rl';
|
sourceRect.top - this._cachedSourceRect.top
|
||||||
default:
|
);
|
||||||
return writingMode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,7 +229,7 @@ class TextScanner extends EventDispatcher {
|
|||||||
getTextSourceContent(textSource, length, layoutAwareScan) {
|
getTextSourceContent(textSource, length, layoutAwareScan) {
|
||||||
const clonedTextSource = textSource.clone();
|
const clonedTextSource = textSource.clone();
|
||||||
|
|
||||||
clonedTextSource.setEndOffset(length, layoutAwareScan, false);
|
clonedTextSource.setEndOffset(length, false, layoutAwareScan);
|
||||||
|
|
||||||
const includeSelector = this._includeSelector;
|
const includeSelector = this._includeSelector;
|
||||||
const excludeSelector = this._excludeSelector;
|
const excludeSelector = this._excludeSelector;
|
||||||
@ -875,7 +875,7 @@ class TextScanner extends EventDispatcher {
|
|||||||
const {dictionaryEntries, originalTextLength} = await yomichan.api.termsFind(searchText, details, optionsContext);
|
const {dictionaryEntries, originalTextLength} = await yomichan.api.termsFind(searchText, details, optionsContext);
|
||||||
if (dictionaryEntries.length === 0) { return null; }
|
if (dictionaryEntries.length === 0) { return null; }
|
||||||
|
|
||||||
textSource.setEndOffset(originalTextLength, layoutAwareScan, false);
|
textSource.setEndOffset(originalTextLength, false, layoutAwareScan);
|
||||||
const sentence = DocumentUtil.extractSentence(
|
const sentence = DocumentUtil.extractSentence(
|
||||||
textSource,
|
textSource,
|
||||||
layoutAwareScan,
|
layoutAwareScan,
|
||||||
@ -902,7 +902,7 @@ class TextScanner extends EventDispatcher {
|
|||||||
const dictionaryEntries = await yomichan.api.kanjiFind(searchText, optionsContext);
|
const dictionaryEntries = await yomichan.api.kanjiFind(searchText, optionsContext);
|
||||||
if (dictionaryEntries.length === 0) { return null; }
|
if (dictionaryEntries.length === 0) { return null; }
|
||||||
|
|
||||||
textSource.setEndOffset(1, layoutAwareScan, false);
|
textSource.setEndOffset(1, false, layoutAwareScan);
|
||||||
const sentence = DocumentUtil.extractSentence(
|
const sentence = DocumentUtil.extractSentence(
|
||||||
textSource,
|
textSource,
|
||||||
layoutAwareScan,
|
layoutAwareScan,
|
||||||
@ -1127,7 +1127,7 @@ class TextScanner extends EventDispatcher {
|
|||||||
(excludeSelector !== null && DocumentUtil.anyNodeMatchesSelector(nodes, excludeSelector))
|
(excludeSelector !== null && DocumentUtil.anyNodeMatchesSelector(nodes, excludeSelector))
|
||||||
) {
|
) {
|
||||||
--length;
|
--length;
|
||||||
textSource.setEndOffset(length, layoutAwareScan, false);
|
textSource.setEndOffset(length, false, layoutAwareScan);
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ class PopupPreviewFrame {
|
|||||||
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.selectNodeContents(textNode);
|
range.selectNodeContents(textNode);
|
||||||
const source = new TextSourceRange(range, range.toString(), null, null);
|
const source = TextSourceRange.create(range);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._frontend.setTextSource(source);
|
await this._frontend.setTextSource(source);
|
||||||
|
Loading…
Reference in New Issue
Block a user