From 2a86d6609210a586ec32c48a99904c9b64744d04 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 9 Aug 2020 21:07:11 -0400 Subject: DOM + DocumentUtil merge (#727) * Add DOM functions to DocumentUtil * Use DocumentUtil instead of DOM * Remove DOM * Move document-util.js into mixed --- dev/data/manifest-variants.json | 3 +- ext/bg/js/search.js | 4 +- ext/bg/js/settings/conditions-ui.js | 6 +- ext/bg/search.html | 3 +- ext/bg/settings-popup-preview.html | 3 +- ext/bg/settings.html | 2 +- ext/fg/float.html | 3 +- ext/fg/js/document-util.js | 381 --------------------------- ext/fg/js/frontend.js | 5 +- ext/fg/js/popup.js | 6 +- ext/manifest.json | 3 +- ext/mixed/js/display.js | 9 +- ext/mixed/js/document-util.js | 506 ++++++++++++++++++++++++++++++++++++ ext/mixed/js/dom.js | 145 ----------- ext/mixed/js/text-scanner.js | 16 +- test/test-document-util.js | 3 +- 16 files changed, 535 insertions(+), 563 deletions(-) delete mode 100644 ext/fg/js/document-util.js create mode 100644 ext/mixed/js/document-util.js delete mode 100644 ext/mixed/js/dom.js diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json index c8135baf..fd11c5c2 100644 --- a/dev/data/manifest-variants.json +++ b/dev/data/manifest-variants.json @@ -42,12 +42,11 @@ "mixed/js/core.js", "mixed/js/yomichan.js", "mixed/js/comm.js", - "mixed/js/dom.js", "mixed/js/api.js", "mixed/js/dynamic-loader.js", "mixed/js/frame-client.js", "mixed/js/text-scanner.js", - "fg/js/document-util.js", + "mixed/js/document-util.js", "fg/js/dom-text-scanner.js", "fg/js/popup.js", "fg/js/source.js", diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 7cad2671..0a0699a9 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -17,8 +17,8 @@ /* global * ClipboardMonitor - * DOM * Display + * DocumentUtil * api * wanakana */ @@ -104,7 +104,7 @@ class DisplaySearch extends Display { } onKeyDown(e) { - const key = DOM.getKeyFromEvent(e); + const key = DocumentUtil.getKeyFromEvent(e); const ignoreKeys = this._onKeyDownIgnoreKeys; const activeModifierMap = new Map([ diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js index 031689a7..98b3d432 100644 --- a/ext/bg/js/settings/conditions-ui.js +++ b/ext/bg/js/settings/conditions-ui.js @@ -16,7 +16,7 @@ */ /* global - * DOM + * DocumentUtil * conditionsNormalizeOptionValue */ @@ -323,7 +323,7 @@ ConditionsUI.Condition = class Condition { const pressedKeyIndices = new Set(); const onKeyDown = ({originalEvent}) => { - const pressedKeyEventName = DOM.getKeyFromEvent(originalEvent); + const pressedKeyEventName = DocumentUtil.getKeyFromEvent(originalEvent); if (pressedKeyEventName === 'Escape' || pressedKeyEventName === 'Backspace') { pressedKeyIndices.clear(); inputInner.val(''); @@ -331,7 +331,7 @@ ConditionsUI.Condition = class Condition { return; } - const pressedModifiers = DOM.getActiveModifiers(originalEvent); + const pressedModifiers = DocumentUtil.getActiveModifiers(originalEvent); // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey // https://askubuntu.com/questions/567731/why-is-shift-alt-being-mapped-to-meta // It works with mouse events on some platforms, so try to determine if metaKey is pressed diff --git a/ext/bg/search.html b/ext/bg/search.html index 9556a1a1..6f7e04c9 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -73,12 +73,11 @@ - - + diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index 3479efa4..59924a27 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -121,13 +121,12 @@ - - + diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 8326a65c..f6025f55 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1135,7 +1135,6 @@ - @@ -1148,6 +1147,7 @@ + diff --git a/ext/fg/float.html b/ext/fg/float.html index 51aa2350..bd2508e6 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -47,11 +47,10 @@ - - + diff --git a/ext/fg/js/document-util.js b/ext/fg/js/document-util.js deleted file mode 100644 index d3bba30f..00000000 --- a/ext/fg/js/document-util.js +++ /dev/null @@ -1,381 +0,0 @@ -/* - * Copyright (C) 2016-2020 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/* global - * DOM - * DOMTextScanner - * TextSourceElement - * TextSourceRange - */ - -class DocumentUtil { - constructor() { - this._transparentColorPattern = /rgba\s*\([^)]*,\s*0(?:\.0+)?\s*\)/; - } - - getRangeFromPoint(x, y, deepContentScan) { - const elements = this._getElementsFromPoint(x, y, deepContentScan); - let imposter = null; - let imposterContainer = null; - let imposterSourceElement = null; - if (elements.length > 0) { - const element = elements[0]; - switch (element.nodeName.toUpperCase()) { - case 'IMG': - case 'BUTTON': - return new TextSourceElement(element); - case 'INPUT': - imposterSourceElement = element; - [imposter, imposterContainer] = this._createImposter(element, false); - break; - case 'TEXTAREA': - imposterSourceElement = element; - [imposter, imposterContainer] = this._createImposter(element, true); - break; - } - } - - const range = this._caretRangeFromPointExt(x, y, deepContentScan ? elements : []); - if (range !== null) { - if (imposter !== null) { - this._setImposterStyle(imposterContainer.style, 'z-index', '-2147483646'); - this._setImposterStyle(imposter.style, 'pointer-events', 'none'); - } - return new TextSourceRange(range, '', imposterContainer, imposterSourceElement); - } else { - if (imposterContainer !== null) { - imposterContainer.parentNode.removeChild(imposterContainer); - } - return null; - } - } - - extractSentence(source, extent, layoutAwareScan) { - const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; - const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; - const terminators = '…。..??!!'; - - const sourceLocal = source.clone(); - const position = sourceLocal.setStartOffset(extent, layoutAwareScan); - sourceLocal.setEndOffset(extent * 2 - position, layoutAwareScan, true); - const content = sourceLocal.text(); - - let quoteStack = []; - - let startPos = 0; - for (let i = position; i >= startPos; --i) { - const c = content[i]; - - if (c === '\n') { - startPos = i + 1; - break; - } - - if (quoteStack.length === 0 && (terminators.includes(c) || c in quotesFwd)) { - startPos = i + 1; - break; - } - - if (quoteStack.length > 0 && c === quoteStack[0]) { - quoteStack.pop(); - } else if (c in quotesBwd) { - quoteStack.unshift(quotesBwd[c]); - } - } - - quoteStack = []; - - let endPos = content.length; - for (let i = position; i <= endPos; ++i) { - const c = content[i]; - - if (c === '\n') { - endPos = i + 1; - break; - } - - if (quoteStack.length === 0) { - if (terminators.includes(c)) { - endPos = i + 1; - break; - } else if (c in quotesBwd) { - endPos = i; - break; - } - } - - if (quoteStack.length > 0 && c === quoteStack[0]) { - quoteStack.pop(); - } else if (c in quotesFwd) { - quoteStack.unshift(quotesFwd[c]); - } - } - - const text = content.substring(startPos, endPos); - const padding = text.length - text.replace(/^\s+/, '').length; - - return { - text: text.trim(), - offset: position - startPos - padding - }; - } - - // Private - - _setImposterStyle(style, propertyName, value) { - style.setProperty(propertyName, value, 'important'); - } - - _createImposter(element, isTextarea) { - const body = document.body; - if (body === null) { return [null, null]; } - - const elementStyle = window.getComputedStyle(element); - const elementRect = element.getBoundingClientRect(); - const documentRect = document.documentElement.getBoundingClientRect(); - let left = elementRect.left - documentRect.left; - let top = elementRect.top - documentRect.top; - - // Container - const container = document.createElement('div'); - const containerStyle = container.style; - this._setImposterStyle(containerStyle, 'all', 'initial'); - this._setImposterStyle(containerStyle, 'position', 'absolute'); - this._setImposterStyle(containerStyle, 'left', '0'); - this._setImposterStyle(containerStyle, 'top', '0'); - this._setImposterStyle(containerStyle, 'width', `${documentRect.width}px`); - this._setImposterStyle(containerStyle, 'height', `${documentRect.height}px`); - this._setImposterStyle(containerStyle, 'overflow', 'hidden'); - this._setImposterStyle(containerStyle, 'opacity', '0'); - this._setImposterStyle(containerStyle, 'pointer-events', 'none'); - this._setImposterStyle(containerStyle, 'z-index', '2147483646'); - - // Imposter - const imposter = document.createElement('div'); - const imposterStyle = imposter.style; - - let value = element.value; - if (value.endsWith('\n')) { value += '\n'; } - imposter.textContent = value; - - for (let i = 0, ii = elementStyle.length; i < ii; ++i) { - const property = elementStyle[i]; - this._setImposterStyle(imposterStyle, property, elementStyle.getPropertyValue(property)); - } - this._setImposterStyle(imposterStyle, 'position', 'absolute'); - this._setImposterStyle(imposterStyle, 'top', `${top}px`); - this._setImposterStyle(imposterStyle, 'left', `${left}px`); - this._setImposterStyle(imposterStyle, 'margin', '0'); - this._setImposterStyle(imposterStyle, 'pointer-events', 'auto'); - - if (isTextarea) { - if (elementStyle.overflow === 'visible') { - this._setImposterStyle(imposterStyle, 'overflow', 'auto'); - } - } else { - this._setImposterStyle(imposterStyle, 'overflow', 'hidden'); - this._setImposterStyle(imposterStyle, 'white-space', 'nowrap'); - this._setImposterStyle(imposterStyle, 'line-height', elementStyle.height); - } - - container.appendChild(imposter); - body.appendChild(container); - - // Adjust size - const imposterRect = imposter.getBoundingClientRect(); - if (imposterRect.width !== elementRect.width || imposterRect.height !== elementRect.height) { - const width = parseFloat(elementStyle.width) + (elementRect.width - imposterRect.width); - const height = parseFloat(elementStyle.height) + (elementRect.height - imposterRect.height); - this._setImposterStyle(imposterStyle, 'width', `${width}px`); - this._setImposterStyle(imposterStyle, 'height', `${height}px`); - } - if (imposterRect.x !== elementRect.x || imposterRect.y !== elementRect.y) { - left += (elementRect.left - imposterRect.left); - top += (elementRect.top - imposterRect.top); - this._setImposterStyle(imposterStyle, 'left', `${left}px`); - this._setImposterStyle(imposterStyle, 'top', `${top}px`); - } - - imposter.scrollTop = element.scrollTop; - imposter.scrollLeft = element.scrollLeft; - - return [imposter, container]; - } - - _getElementsFromPoint(x, y, all) { - if (all) { - // document.elementsFromPoint can return duplicates which must be removed. - const elements = document.elementsFromPoint(x, y); - return elements.filter((e, i) => elements.indexOf(e) === i); - } - - const e = document.elementFromPoint(x, y); - return e !== null ? [e] : []; - } - - _isPointInRange(x, y, range) { - // Require a text node to start - if (range.startContainer.nodeType !== Node.TEXT_NODE) { - return false; - } - - // Scan forward - const nodePre = range.endContainer; - const offsetPre = range.endOffset; - try { - const {node, offset, content} = new DOMTextScanner(range.endContainer, range.endOffset, true, false).seek(1); - range.setEnd(node, offset); - - if (!this._isWhitespace(content) && DOM.isPointInAnyRect(x, y, range.getClientRects())) { - return true; - } - } finally { - range.setEnd(nodePre, offsetPre); - } - - // Scan backward - const {node, offset, content} = new DOMTextScanner(range.startContainer, range.startOffset, true, false).seek(-1); - range.setStart(node, offset); - - if (!this._isWhitespace(content) && DOM.isPointInAnyRect(x, y, range.getClientRects())) { - // This purposefully leaves the starting offset as modified and sets the range length to 0. - range.setEnd(node, offset); - return true; - } - - // No match - return false; - } - - _isWhitespace(string) { - return string.trim().length === 0; - } - - _caretRangeFromPoint(x, y) { - if (typeof document.caretRangeFromPoint === 'function') { - // Chrome, Edge - return document.caretRangeFromPoint(x, y); - } - - if (typeof document.caretPositionFromPoint === 'function') { - // Firefox - return this._caretPositionFromPoint(x, y); - } - - // No support - return null; - } - - _caretPositionFromPoint(x, y) { - const position = document.caretPositionFromPoint(x, y); - if (position === null) { - return null; - } - const node = position.offsetNode; - if (node === null) { - return null; - } - - const range = document.createRange(); - const offset = (node.nodeType === Node.TEXT_NODE ? position.offset : 0); - try { - range.setStart(node, offset); - range.setEnd(node, offset); - } catch (e) { - // Firefox throws new DOMException("The operation is insecure.") - // when trying to select a node from within a ShadowRoot. - return null; - } - return range; - } - - _caretRangeFromPointExt(x, y, elements) { - const modifications = []; - try { - let i = 0; - let startContinerPre = null; - while (true) { - const range = this._caretRangeFromPoint(x, y); - if (range === null) { - return null; - } - - const startContainer = range.startContainer; - if (startContinerPre !== startContainer) { - if (this._isPointInRange(x, y, range)) { - return range; - } - startContinerPre = startContainer; - } - - i = this._disableTransparentElement(elements, i, modifications); - if (i < 0) { - return null; - } - } - } finally { - if (modifications.length > 0) { - this._restoreElementStyleModifications(modifications); - } - } - } - - _disableTransparentElement(elements, i, modifications) { - while (true) { - if (i >= elements.length) { - return -1; - } - - const element = elements[i++]; - if (this._isElementTransparent(element)) { - const style = element.hasAttribute('style') ? element.getAttribute('style') : null; - modifications.push({element, style}); - element.style.setProperty('pointer-events', 'none', 'important'); - return i; - } - } - } - - _restoreElementStyleModifications(modifications) { - for (const {element, style} of modifications) { - if (style === null) { - element.removeAttribute('style'); - } else { - element.setAttribute('style', style); - } - } - } - - _isElementTransparent(element) { - if ( - element === document.body || - element === document.documentElement - ) { - return false; - } - const style = window.getComputedStyle(element); - return ( - parseFloat(style.opacity) <= 0 || - style.visibility === 'hidden' || - (style.backgroundImage === 'none' && this._isColorTransparent(style.backgroundColor)) - ); - } - - _isColorTransparent(cssColor) { - return this._transparentColorPattern.test(cssColor); - } -} diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index ccffbab6..0c18fb2a 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -16,7 +16,6 @@ */ /* global - * DOM * DocumentUtil * FrameOffsetForwarder * PopupProxy @@ -98,7 +97,7 @@ class Frontend { this._textScanner.prepare(); window.addEventListener('resize', this._onResize.bind(this), false); - DOM.addFullscreenChangeEventListener(this._updatePopup.bind(this)); + DocumentUtil.addFullscreenChangeEventListener(this._updatePopup.bind(this)); const visualViewport = window.visualViewport; if (visualViewport !== null && typeof visualViewport === 'object') { @@ -274,7 +273,7 @@ class Frontend { if ( isIframe && showIframePopupsInRootFrame && - DOM.getFullscreenElement() === null && + DocumentUtil.getFullscreenElement() === null && this._allowRootFramePopupProxy ) { popupPromise = this._popupCache.get('iframe'); diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 22672706..8ea1afd0 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -16,7 +16,7 @@ */ /* global - * DOM + * DocumentUtil * FrameClient * api * dynamicLoader @@ -349,7 +349,7 @@ class Popup { return; } - DOM.addFullscreenChangeEventListener(this._onFullscreenChanged.bind(this), this._fullscreenEventListeners); + DocumentUtil.addFullscreenChangeEventListener(this._onFullscreenChanged.bind(this), this._fullscreenEventListeners); } _onFullscreenChanged() { @@ -475,7 +475,7 @@ class Popup { _getFrameParentElement() { const defaultParent = document.body; - const fullscreenElement = DOM.getFullscreenElement(); + const fullscreenElement = DocumentUtil.getFullscreenElement(); if ( fullscreenElement === null || fullscreenElement.shadowRoot || diff --git a/ext/manifest.json b/ext/manifest.json index b22e75c6..12286c40 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -41,12 +41,11 @@ "mixed/js/core.js", "mixed/js/yomichan.js", "mixed/js/comm.js", - "mixed/js/dom.js", "mixed/js/api.js", "mixed/js/dynamic-loader.js", "mixed/js/frame-client.js", "mixed/js/text-scanner.js", - "fg/js/document-util.js", + "mixed/js/document-util.js", "fg/js/dom-text-scanner.js", "fg/js/popup.js", "fg/js/source.js", diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 170b9d23..08ececc7 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -17,7 +17,6 @@ /* global * AudioSystem - * DOM * DisplayGenerator * DisplayHistory * DocumentUtil @@ -186,11 +185,11 @@ class Display extends EventDispatcher { } onKeyDown(e) { - const key = DOM.getKeyFromEvent(e); + const key = DocumentUtil.getKeyFromEvent(e); const handlers = this._hotkeys.get(key); if (typeof handlers === 'undefined') { return false; } - const eventModifiers = DOM.getActiveModifiers(e); + const eventModifiers = DocumentUtil.getActiveModifiers(e); for (const {modifiers, action} of handlers) { if (getSetDifference(modifiers, eventModifiers).size !== 0) { continue; } @@ -558,7 +557,7 @@ class Display extends EventDispatcher { } _onGlossaryMouseDown(e) { - if (DOM.isMouseButtonPressed(e, 'primary')) { + if (DocumentUtil.isMouseButtonPressed(e, 'primary')) { this._clickScanPrevent = false; } } @@ -568,7 +567,7 @@ class Display extends EventDispatcher { } _onGlossaryMouseUp(e) { - if (!this._clickScanPrevent && DOM.isMouseButtonPressed(e, 'primary')) { + if (!this._clickScanPrevent && DocumentUtil.isMouseButtonPressed(e, 'primary')) { try { this._onTermLookup(e); } catch (error) { diff --git a/ext/mixed/js/document-util.js b/ext/mixed/js/document-util.js new file mode 100644 index 00000000..ba39942d --- /dev/null +++ b/ext/mixed/js/document-util.js @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2016-2020 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* global + * DOMTextScanner + * TextSourceElement + * TextSourceRange + */ + +class DocumentUtil { + constructor() { + this._transparentColorPattern = /rgba\s*\([^)]*,\s*0(?:\.0+)?\s*\)/; + } + + getRangeFromPoint(x, y, deepContentScan) { + const elements = this._getElementsFromPoint(x, y, deepContentScan); + let imposter = null; + let imposterContainer = null; + let imposterSourceElement = null; + if (elements.length > 0) { + const element = elements[0]; + switch (element.nodeName.toUpperCase()) { + case 'IMG': + case 'BUTTON': + return new TextSourceElement(element); + case 'INPUT': + imposterSourceElement = element; + [imposter, imposterContainer] = this._createImposter(element, false); + break; + case 'TEXTAREA': + imposterSourceElement = element; + [imposter, imposterContainer] = this._createImposter(element, true); + break; + } + } + + const range = this._caretRangeFromPointExt(x, y, deepContentScan ? elements : []); + if (range !== null) { + if (imposter !== null) { + this._setImposterStyle(imposterContainer.style, 'z-index', '-2147483646'); + this._setImposterStyle(imposter.style, 'pointer-events', 'none'); + } + return new TextSourceRange(range, '', imposterContainer, imposterSourceElement); + } else { + if (imposterContainer !== null) { + imposterContainer.parentNode.removeChild(imposterContainer); + } + return null; + } + } + + extractSentence(source, extent, layoutAwareScan) { + const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; + const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; + const terminators = '…。..??!!'; + + const sourceLocal = source.clone(); + const position = sourceLocal.setStartOffset(extent, layoutAwareScan); + sourceLocal.setEndOffset(extent * 2 - position, layoutAwareScan, true); + const content = sourceLocal.text(); + + let quoteStack = []; + + let startPos = 0; + for (let i = position; i >= startPos; --i) { + const c = content[i]; + + if (c === '\n') { + startPos = i + 1; + break; + } + + if (quoteStack.length === 0 && (terminators.includes(c) || c in quotesFwd)) { + startPos = i + 1; + break; + } + + if (quoteStack.length > 0 && c === quoteStack[0]) { + quoteStack.pop(); + } else if (c in quotesBwd) { + quoteStack.unshift(quotesBwd[c]); + } + } + + quoteStack = []; + + let endPos = content.length; + for (let i = position; i <= endPos; ++i) { + const c = content[i]; + + if (c === '\n') { + endPos = i + 1; + break; + } + + if (quoteStack.length === 0) { + if (terminators.includes(c)) { + endPos = i + 1; + break; + } else if (c in quotesBwd) { + endPos = i; + break; + } + } + + if (quoteStack.length > 0 && c === quoteStack[0]) { + quoteStack.pop(); + } else if (c in quotesFwd) { + quoteStack.unshift(quotesFwd[c]); + } + } + + const text = content.substring(startPos, endPos); + const padding = text.length - text.replace(/^\s+/, '').length; + + return { + text: text.trim(), + offset: position - startPos - padding + }; + } + + static isPointInRect(x, y, rect) { + return ( + x >= rect.left && x < rect.right && + y >= rect.top && y < rect.bottom + ); + } + + static isPointInAnyRect(x, y, rects) { + for (const rect of rects) { + if (this.isPointInRect(x, y, rect)) { + return true; + } + } + return false; + } + + static isPointInSelection(x, y, selection) { + for (let i = 0; i < selection.rangeCount; ++i) { + const range = selection.getRangeAt(i); + if (this.isPointInAnyRect(x, y, range.getClientRects())) { + return true; + } + } + return false; + } + + static isMouseButtonPressed(mouseEvent, button) { + const mouseEventButton = mouseEvent.button; + switch (button) { + case 'primary': return mouseEventButton === 0; + case 'secondary': return mouseEventButton === 2; + case 'auxiliary': return mouseEventButton === 1; + default: return false; + } + } + + static isMouseButtonDown(mouseEvent, button) { + const mouseEventButtons = mouseEvent.buttons; + switch (button) { + case 'primary': return (mouseEventButtons & 0x1) !== 0x0; + case 'secondary': return (mouseEventButtons & 0x2) !== 0x0; + case 'auxiliary': return (mouseEventButtons & 0x4) !== 0x0; + default: return false; + } + } + + static getActiveModifiers(event) { + const modifiers = new Set(); + if (event.altKey) { modifiers.add('alt'); } + if (event.ctrlKey) { modifiers.add('ctrl'); } + if (event.metaKey) { modifiers.add('meta'); } + if (event.shiftKey) { modifiers.add('shift'); } + return modifiers; + } + + static getKeyFromEvent(event) { + const key = event.key; + return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : ''); + } + + static addFullscreenChangeEventListener(onFullscreenChanged, eventListenerCollection=null) { + const target = document; + const options = false; + const fullscreenEventNames = [ + 'fullscreenchange', + 'MSFullscreenChange', + 'mozfullscreenchange', + 'webkitfullscreenchange' + ]; + for (const eventName of fullscreenEventNames) { + if (eventListenerCollection === null) { + target.addEventListener(eventName, onFullscreenChanged, options); + } else { + eventListenerCollection.addEventListener(target, eventName, onFullscreenChanged, options); + } + } + } + + static getFullscreenElement() { + return ( + document.fullscreenElement || + document.msFullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + null + ); + } + + static getNodesInRange(range) { + const end = range.endContainer; + const nodes = []; + for (let node = range.startContainer; node !== null; node = this.getNextNode(node)) { + nodes.push(node); + if (node === end) { break; } + } + return nodes; + } + + static getNextNode(node) { + let next = node.firstChild; + if (next === null) { + while (true) { + next = node.nextSibling; + if (next !== null) { break; } + + next = node.parentNode; + if (next === null) { break; } + + node = next; + } + } + return next; + } + + static anyNodeMatchesSelector(nodes, selector) { + const ELEMENT_NODE = Node.ELEMENT_NODE; + for (let node of nodes) { + for (; node !== null; node = node.parentNode) { + if (node.nodeType !== ELEMENT_NODE) { continue; } + if (node.matches(selector)) { return true; } + break; + } + } + return false; + } + + // Private + + _setImposterStyle(style, propertyName, value) { + style.setProperty(propertyName, value, 'important'); + } + + _createImposter(element, isTextarea) { + const body = document.body; + if (body === null) { return [null, null]; } + + const elementStyle = window.getComputedStyle(element); + const elementRect = element.getBoundingClientRect(); + const documentRect = document.documentElement.getBoundingClientRect(); + let left = elementRect.left - documentRect.left; + let top = elementRect.top - documentRect.top; + + // Container + const container = document.createElement('div'); + const containerStyle = container.style; + this._setImposterStyle(containerStyle, 'all', 'initial'); + this._setImposterStyle(containerStyle, 'position', 'absolute'); + this._setImposterStyle(containerStyle, 'left', '0'); + this._setImposterStyle(containerStyle, 'top', '0'); + this._setImposterStyle(containerStyle, 'width', `${documentRect.width}px`); + this._setImposterStyle(containerStyle, 'height', `${documentRect.height}px`); + this._setImposterStyle(containerStyle, 'overflow', 'hidden'); + this._setImposterStyle(containerStyle, 'opacity', '0'); + this._setImposterStyle(containerStyle, 'pointer-events', 'none'); + this._setImposterStyle(containerStyle, 'z-index', '2147483646'); + + // Imposter + const imposter = document.createElement('div'); + const imposterStyle = imposter.style; + + let value = element.value; + if (value.endsWith('\n')) { value += '\n'; } + imposter.textContent = value; + + for (let i = 0, ii = elementStyle.length; i < ii; ++i) { + const property = elementStyle[i]; + this._setImposterStyle(imposterStyle, property, elementStyle.getPropertyValue(property)); + } + this._setImposterStyle(imposterStyle, 'position', 'absolute'); + this._setImposterStyle(imposterStyle, 'top', `${top}px`); + this._setImposterStyle(imposterStyle, 'left', `${left}px`); + this._setImposterStyle(imposterStyle, 'margin', '0'); + this._setImposterStyle(imposterStyle, 'pointer-events', 'auto'); + + if (isTextarea) { + if (elementStyle.overflow === 'visible') { + this._setImposterStyle(imposterStyle, 'overflow', 'auto'); + } + } else { + this._setImposterStyle(imposterStyle, 'overflow', 'hidden'); + this._setImposterStyle(imposterStyle, 'white-space', 'nowrap'); + this._setImposterStyle(imposterStyle, 'line-height', elementStyle.height); + } + + container.appendChild(imposter); + body.appendChild(container); + + // Adjust size + const imposterRect = imposter.getBoundingClientRect(); + if (imposterRect.width !== elementRect.width || imposterRect.height !== elementRect.height) { + const width = parseFloat(elementStyle.width) + (elementRect.width - imposterRect.width); + const height = parseFloat(elementStyle.height) + (elementRect.height - imposterRect.height); + this._setImposterStyle(imposterStyle, 'width', `${width}px`); + this._setImposterStyle(imposterStyle, 'height', `${height}px`); + } + if (imposterRect.x !== elementRect.x || imposterRect.y !== elementRect.y) { + left += (elementRect.left - imposterRect.left); + top += (elementRect.top - imposterRect.top); + this._setImposterStyle(imposterStyle, 'left', `${left}px`); + this._setImposterStyle(imposterStyle, 'top', `${top}px`); + } + + imposter.scrollTop = element.scrollTop; + imposter.scrollLeft = element.scrollLeft; + + return [imposter, container]; + } + + _getElementsFromPoint(x, y, all) { + if (all) { + // document.elementsFromPoint can return duplicates which must be removed. + const elements = document.elementsFromPoint(x, y); + return elements.filter((e, i) => elements.indexOf(e) === i); + } + + const e = document.elementFromPoint(x, y); + return e !== null ? [e] : []; + } + + _isPointInRange(x, y, range) { + // Require a text node to start + if (range.startContainer.nodeType !== Node.TEXT_NODE) { + return false; + } + + // Scan forward + const nodePre = range.endContainer; + const offsetPre = range.endOffset; + try { + const {node, offset, content} = new DOMTextScanner(range.endContainer, range.endOffset, true, false).seek(1); + range.setEnd(node, offset); + + if (!this._isWhitespace(content) && DocumentUtil.isPointInAnyRect(x, y, range.getClientRects())) { + return true; + } + } finally { + range.setEnd(nodePre, offsetPre); + } + + // Scan backward + const {node, offset, content} = new DOMTextScanner(range.startContainer, range.startOffset, true, false).seek(-1); + range.setStart(node, offset); + + if (!this._isWhitespace(content) && DocumentUtil.isPointInAnyRect(x, y, range.getClientRects())) { + // This purposefully leaves the starting offset as modified and sets the range length to 0. + range.setEnd(node, offset); + return true; + } + + // No match + return false; + } + + _isWhitespace(string) { + return string.trim().length === 0; + } + + _caretRangeFromPoint(x, y) { + if (typeof document.caretRangeFromPoint === 'function') { + // Chrome, Edge + return document.caretRangeFromPoint(x, y); + } + + if (typeof document.caretPositionFromPoint === 'function') { + // Firefox + return this._caretPositionFromPoint(x, y); + } + + // No support + return null; + } + + _caretPositionFromPoint(x, y) { + const position = document.caretPositionFromPoint(x, y); + if (position === null) { + return null; + } + const node = position.offsetNode; + if (node === null) { + return null; + } + + const range = document.createRange(); + const offset = (node.nodeType === Node.TEXT_NODE ? position.offset : 0); + try { + range.setStart(node, offset); + range.setEnd(node, offset); + } catch (e) { + // Firefox throws new DOMException("The operation is insecure.") + // when trying to select a node from within a ShadowRoot. + return null; + } + return range; + } + + _caretRangeFromPointExt(x, y, elements) { + const modifications = []; + try { + let i = 0; + let startContinerPre = null; + while (true) { + const range = this._caretRangeFromPoint(x, y); + if (range === null) { + return null; + } + + const startContainer = range.startContainer; + if (startContinerPre !== startContainer) { + if (this._isPointInRange(x, y, range)) { + return range; + } + startContinerPre = startContainer; + } + + i = this._disableTransparentElement(elements, i, modifications); + if (i < 0) { + return null; + } + } + } finally { + if (modifications.length > 0) { + this._restoreElementStyleModifications(modifications); + } + } + } + + _disableTransparentElement(elements, i, modifications) { + while (true) { + if (i >= elements.length) { + return -1; + } + + const element = elements[i++]; + if (this._isElementTransparent(element)) { + const style = element.hasAttribute('style') ? element.getAttribute('style') : null; + modifications.push({element, style}); + element.style.setProperty('pointer-events', 'none', 'important'); + return i; + } + } + } + + _restoreElementStyleModifications(modifications) { + for (const {element, style} of modifications) { + if (style === null) { + element.removeAttribute('style'); + } else { + element.setAttribute('style', style); + } + } + } + + _isElementTransparent(element) { + if ( + element === document.body || + element === document.documentElement + ) { + return false; + } + const style = window.getComputedStyle(element); + return ( + parseFloat(style.opacity) <= 0 || + style.visibility === 'hidden' || + (style.backgroundImage === 'none' && this._isColorTransparent(style.backgroundColor)) + ); + } + + _isColorTransparent(cssColor) { + return this._transparentColorPattern.test(cssColor); + } +} diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js deleted file mode 100644 index 59fea9f6..00000000 --- a/ext/mixed/js/dom.js +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2019-2020 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - - -class DOM { - static isPointInRect(x, y, rect) { - return ( - x >= rect.left && x < rect.right && - y >= rect.top && y < rect.bottom - ); - } - - static isPointInAnyRect(x, y, rects) { - for (const rect of rects) { - if (DOM.isPointInRect(x, y, rect)) { - return true; - } - } - return false; - } - - static isPointInSelection(x, y, selection) { - for (let i = 0; i < selection.rangeCount; ++i) { - const range = selection.getRangeAt(i); - if (DOM.isPointInAnyRect(x, y, range.getClientRects())) { - return true; - } - } - return false; - } - - static isMouseButtonPressed(mouseEvent, button) { - const mouseEventButton = mouseEvent.button; - switch (button) { - case 'primary': return mouseEventButton === 0; - case 'secondary': return mouseEventButton === 2; - case 'auxiliary': return mouseEventButton === 1; - default: return false; - } - } - - static isMouseButtonDown(mouseEvent, button) { - const mouseEventButtons = mouseEvent.buttons; - switch (button) { - case 'primary': return (mouseEventButtons & 0x1) !== 0x0; - case 'secondary': return (mouseEventButtons & 0x2) !== 0x0; - case 'auxiliary': return (mouseEventButtons & 0x4) !== 0x0; - default: return false; - } - } - - static getActiveModifiers(event) { - const modifiers = new Set(); - if (event.altKey) { modifiers.add('alt'); } - if (event.ctrlKey) { modifiers.add('ctrl'); } - if (event.metaKey) { modifiers.add('meta'); } - if (event.shiftKey) { modifiers.add('shift'); } - return modifiers; - } - - static getKeyFromEvent(event) { - const key = event.key; - return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : ''); - } - - static addFullscreenChangeEventListener(onFullscreenChanged, eventListenerCollection=null) { - const target = document; - const options = false; - const fullscreenEventNames = [ - 'fullscreenchange', - 'MSFullscreenChange', - 'mozfullscreenchange', - 'webkitfullscreenchange' - ]; - for (const eventName of fullscreenEventNames) { - if (eventListenerCollection === null) { - target.addEventListener(eventName, onFullscreenChanged, options); - } else { - eventListenerCollection.addEventListener(target, eventName, onFullscreenChanged, options); - } - } - } - - static getFullscreenElement() { - return ( - document.fullscreenElement || - document.msFullscreenElement || - document.mozFullScreenElement || - document.webkitFullscreenElement || - null - ); - } - - static getNodesInRange(range) { - const end = range.endContainer; - const nodes = []; - for (let node = range.startContainer; node !== null; node = DOM.getNextNode(node)) { - nodes.push(node); - if (node === end) { break; } - } - return nodes; - } - - static getNextNode(node) { - let next = node.firstChild; - if (next === null) { - while (true) { - next = node.nextSibling; - if (next !== null) { break; } - - next = node.parentNode; - if (next === null) { break; } - - node = next; - } - } - return next; - } - - static anyNodeMatchesSelector(nodes, selector) { - const ELEMENT_NODE = Node.ELEMENT_NODE; - for (let node of nodes) { - for (; node !== null; node = node.parentNode) { - if (node.nodeType !== ELEMENT_NODE) { continue; } - if (node.matches(selector)) { return true; } - break; - } - } - return false; - } -} diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 923784b3..d0256b1e 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -16,7 +16,7 @@ */ /* global - * DOM + * DocumentUtil */ class TextScanner extends EventDispatcher { @@ -155,8 +155,8 @@ class TextScanner extends EventDispatcher { if (this._ignoreNodes !== null && clonedTextSource.range) { length = clonedTextSource.text().length; while (clonedTextSource.range && length > 0) { - const nodes = DOM.getNodesInRange(clonedTextSource.range); - if (!DOM.anyNodeMatchesSelector(nodes, this._ignoreNodes)) { + const nodes = DocumentUtil.getNodesInRange(clonedTextSource.range); + if (!DocumentUtil.anyNodeMatchesSelector(nodes, this._ignoreNodes)) { break; } --length; @@ -204,16 +204,16 @@ class TextScanner extends EventDispatcher { _onMouseMove(e) { this._scanTimerClear(); - if (this._pendingLookup || DOM.isMouseButtonDown(e, 'primary')) { + if (this._pendingLookup || DocumentUtil.isMouseButtonDown(e, 'primary')) { return; } - const modifiers = DOM.getActiveModifiers(e); + const modifiers = DocumentUtil.getActiveModifiers(e); this.trigger('activeModifiersChanged', {modifiers}); if (!( this._isScanningModifierPressed(this._modifier, e) || - (this._useMiddleMouse && DOM.isMouseButtonDown(e, 'auxiliary')) + (this._useMiddleMouse && DocumentUtil.isMouseButtonDown(e, 'auxiliary')) )) { return; } @@ -241,7 +241,7 @@ class TextScanner extends EventDispatcher { return false; } - if (DOM.isMouseButtonDown(e, 'primary')) { + if (DocumentUtil.isMouseButtonDown(e, 'primary')) { this._scanTimerClear(); this.clearSelection(false); } @@ -284,7 +284,7 @@ class TextScanner extends EventDispatcher { this._preventNextClick = false; const primaryTouch = e.changedTouches[0]; - if (DOM.isPointInSelection(primaryTouch.clientX, primaryTouch.clientY, window.getSelection())) { + if (DocumentUtil.isPointInSelection(primaryTouch.clientX, primaryTouch.clientY, window.getSelection())) { return; } diff --git a/test/test-document-util.js b/test/test-document-util.js index 40b65ed1..4ff380ec 100644 --- a/test/test-document-util.js +++ b/test/test-document-util.js @@ -93,10 +93,9 @@ async function testDocument1() { const vm = new VM({document, window, Range, Node}); vm.execute([ - 'mixed/js/dom.js', 'fg/js/dom-text-scanner.js', 'fg/js/source.js', - 'fg/js/document-util.js' + 'mixed/js/document-util.js' ]); const [DOMTextScanner, TextSourceRange, TextSourceElement, DocumentUtil] = vm.get([ 'DOMTextScanner', -- cgit v1.2.3