diff options
Diffstat (limited to 'ext/fg')
-rw-r--r-- | ext/fg/css/client.css | 17 | ||||
-rw-r--r-- | ext/fg/float.html | 12 | ||||
-rw-r--r-- | ext/fg/js/api.js | 12 | ||||
-rw-r--r-- | ext/fg/js/document.js | 10 | ||||
-rw-r--r-- | ext/fg/js/float.js | 51 | ||||
-rw-r--r-- | ext/fg/js/frontend-initialize.js | 20 | ||||
-rw-r--r-- | ext/fg/js/frontend.js | 61 | ||||
-rw-r--r-- | ext/fg/js/popup-nested.js | 5 | ||||
-rw-r--r-- | ext/fg/js/popup-proxy-host.js | 44 | ||||
-rw-r--r-- | ext/fg/js/popup-proxy.js | 25 | ||||
-rw-r--r-- | ext/fg/js/popup.js | 144 | ||||
-rw-r--r-- | ext/fg/js/source.js | 26 |
12 files changed, 281 insertions, 146 deletions
diff --git a/ext/fg/css/client.css b/ext/fg/css/client.css index a2b06d0f..633c88ef 100644 --- a/ext/fg/css/client.css +++ b/ext/fg/css/client.css @@ -17,11 +17,11 @@ */ -iframe#yomichan-float { +iframe.yomichan-float { all: initial; background-color: #fff; border: 1px solid #999; - box-shadow: 0 0 10px rgba(0, 0, 0, .5); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); position: fixed; resize: both; visibility: hidden; @@ -29,7 +29,14 @@ iframe#yomichan-float { box-sizing: border-box; } -iframe#yomichan-float.yomichan-float-full-width { +iframe.yomichan-float[data-yomichan-theme=dark], +iframe.yomichan-float[data-yomichan-theme=auto][data-yomichan-site-color=dark] { + background-color: #1e1e1e; + border: 1px solid #666; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); +} + +iframe.yomichan-float.yomichan-float-full-width { border-left: none; border-right: none; left: 0 !important; @@ -39,13 +46,13 @@ iframe#yomichan-float.yomichan-float-full-width { resize: none; } -iframe#yomichan-float.yomichan-float-full-width:not(.yomichan-float-above) { +iframe.yomichan-float.yomichan-float-full-width:not(.yomichan-float-above) { border-bottom: none; top: auto !important; bottom: 0 !important; } -iframe#yomichan-float.yomichan-float-full-width.yomichan-float-above { +iframe.yomichan-float.yomichan-float-full-width.yomichan-float-above { border-top: none; top: 0 !important; bottom: auto !important; diff --git a/ext/fg/float.html b/ext/fg/float.html index fe1aee8f..580a7963 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -1,18 +1,12 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" data-yomichan-page="float"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title></title> - <link rel="stylesheet" href="/mixed/lib/bootstrap/css/bootstrap.min.css"> - <link rel="stylesheet" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css"> <link rel="stylesheet" href="/mixed/css/display.css"> - <style type="text/css"> - .entry, .note { - padding-left: 10px; - padding-right: 10px; - } - </style> + <link rel="stylesheet" type="text/css" href="/mixed/css/display-default.css" data-yomichan-theme-name="default"> + <link rel="stylesheet alternate" type="text/css" href="/mixed/css/display-dark.css" data-yomichan-theme-name="dark"> </head> <body> <div id="spinner"> diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js index a553e514..b0746b85 100644 --- a/ext/fg/js/api.js +++ b/ext/fg/js/api.js @@ -49,8 +49,8 @@ function apiAudioGetUrl(definition, source, optionsContext) { return utilInvoke('audioGetUrl', {definition, source, optionsContext}); } -function apiCommandExec(command) { - return utilInvoke('commandExec', {command}); +function apiCommandExec(command, params) { + return utilInvoke('commandExec', {command, params}); } function apiScreenshotGet(options) { @@ -64,3 +64,11 @@ function apiForward(action, params) { function apiFrameInformationGet() { return utilInvoke('frameInformationGet'); } + +function apiInjectStylesheet(css) { + return utilInvoke('injectStylesheet', {css}); +} + +function apiGetEnvironmentInfo() { + return utilInvoke('getEnvironmentInfo'); +} diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index 94a68e6c..a168705e 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -27,8 +27,8 @@ function docImposterCreate(element, isTextarea) { const elementStyle = window.getComputedStyle(element); const elementRect = element.getBoundingClientRect(); const documentRect = document.documentElement.getBoundingClientRect(); - const left = elementRect.left - documentRect.left; - const top = elementRect.top - documentRect.top; + let left = elementRect.left - documentRect.left; + let top = elementRect.top - documentRect.top; // Container const container = document.createElement('div'); @@ -82,6 +82,12 @@ function docImposterCreate(element, isTextarea) { docSetImposterStyle(imposterStyle, 'width', `${width}px`); docSetImposterStyle(imposterStyle, 'height', `${height}px`); } + if (imposterRect.x !== elementRect.x || imposterRect.y !== elementRect.y) { + left += (elementRect.left - imposterRect.left); + top += (elementRect.top - imposterRect.top); + docSetImposterStyle(imposterStyle, 'left', `${left}px`); + docSetImposterStyle(imposterStyle, 'top', `${top}px`); + } imposter.scrollTop = element.scrollTop; imposter.scrollLeft = element.scrollLeft; diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 8fdb6925..089c9422 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -21,39 +21,23 @@ class DisplayFloat extends Display { constructor() { super(document.querySelector('#spinner'), document.querySelector('#definitions')); this.autoPlayAudioTimer = null; - this.styleNode = null; this.optionsContext = { depth: 0, url: window.location.href }; - this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract}); - window.addEventListener('message', (e) => this.onMessage(e), false); } onError(error) { if (window.yomichan_orphaned) { - this.onOrphaned(); + this.setContentOrphaned(); } else { logError(error, true); } } - onOrphaned() { - const definitions = document.querySelector('#definitions'); - const errorOrphaned = document.querySelector('#error-orphaned'); - - if (definitions !== null) { - definitions.style.setProperty('display', 'none', 'important'); - } - - if (errorOrphaned !== null) { - errorOrphaned.style.setProperty('display', 'block', 'important'); - } - } - onSearchClear() { window.parent.postMessage('popupClose', '*'); } @@ -84,6 +68,10 @@ class DisplayFloat extends Display { super.onKeyDown(e); } + getOptionsContext() { + return this.optionsContext; + } + autoPlayAudio() { this.clearAutoPlayTimer(); this.autoPlayAudioTimer = window.setTimeout(() => super.autoPlayAudio(), 400); @@ -96,29 +84,15 @@ class DisplayFloat extends Display { } } - initialize(options, popupInfo, url) { - const css = options.general.customPopupCss; - if (css) { - this.setStyle(css); - } + async initialize(options, popupInfo, url, childrenSupported) { + await super.initialize(options); const {id, depth, parentFrameId} = popupInfo; this.optionsContext.depth = depth; this.optionsContext.url = url; - popupNestedInitialize(id, depth, parentFrameId, url); - } - - setStyle(css) { - const parent = document.head; - - if (this.styleNode === null) { - this.styleNode = document.createElement('style'); - } - - this.styleNode.textContent = css; - if (this.styleNode.parentNode !== parent) { - parent.appendChild(this.styleNode); + if (childrenSupported) { + popupNestedInitialize(id, depth, parentFrameId, url); } } } @@ -134,11 +108,10 @@ DisplayFloat.onKeyDownHandlers = { }; DisplayFloat.messageHandlers = { - termsShow: (self, {definitions, options, context}) => self.termsShow(definitions, options, context), - kanjiShow: (self, {definitions, options, context}) => self.kanjiShow(definitions, options, context), + setContent: (self, {type, details}) => self.setContent(type, details), clearAutoPlayTimer: (self) => self.clearAutoPlayTimer(), - orphaned: (self) => self.onOrphaned(), - initialize: (self, {options, popupInfo, url}) => self.initialize(options, popupInfo, url) + setCustomCss: (self, {css}) => self.setCustomCss(css), + initialize: (self, {options, popupInfo, url, childrenSupported}) => self.initialize(options, popupInfo, url, childrenSupported) }; window.yomichan_display = new DisplayFloat(); diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js new file mode 100644 index 00000000..37a82faa --- /dev/null +++ b/ext/fg/js/frontend-initialize.js @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 <http://www.gnu.org/licenses/>. + */ + + +window.yomichan_frontend = Frontend.create(); diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 88cb93a9..e854f74e 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -41,14 +41,18 @@ class Frontend { this.enabled = false; this.eventListeners = []; + + this.isPreparedPromiseResolve = null; + this.isPreparedPromise = new Promise((resolve) => { this.isPreparedPromiseResolve = resolve; }); + + this.lastShowPromise = Promise.resolve(); } static create() { - const initializationData = window.frontendInitializationData; - const isNested = (initializationData !== null && typeof initializationData === 'object'); - const {id, depth, parentFrameId, ignoreNodes, url} = isNested ? initializationData : {}; + const data = window.frontendInitializationData || {}; + const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data; - const popup = isNested ? new PopupProxy(depth + 1, id, parentFrameId, url) : PopupProxyHost.instance.createPopup(null); + const popup = proxy ? new PopupProxy(depth + 1, id, parentFrameId, url) : PopupProxyHost.instance.createPopup(null, depth); const frontend = new Frontend(popup, ignoreNodes); frontend.prepare(); return frontend; @@ -59,11 +63,16 @@ class Frontend { await this.updateOptions(); chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); + this.isPreparedPromiseResolve(); } catch (e) { this.onError(e); } } + isPrepared() { + return this.isPreparedPromise; + } + onMouseOver(e) { if (e.target === this.popup.container && this.popupTimer !== null) { this.popupTimerClear(); @@ -130,8 +139,14 @@ class Frontend { } } - onResize() { - this.searchClear(false); + async onResize() { + if (this.textSourceLast !== null && await this.popup.isVisibleAsync()) { + const textSource = this.textSourceLast; + this.lastShowPromise = this.popup.showContent( + textSource.getRect(), + textSource.getWritingMode() + ); + } } onClick(e) { @@ -222,8 +237,8 @@ class Frontend { const handlers = Frontend.runtimeMessageHandlers; if (handlers.hasOwnProperty(action)) { const handler = handlers[action]; - handler(this, params); - callback(); + const result = handler(this, params); + callback(result); } } @@ -279,6 +294,7 @@ class Frontend { async updateOptions() { this.options = await apiOptionsGet(this.getOptionsContext()); this.setEnabled(this.options.general.enable); + await this.popup.setOptions(this.options); } popupTimerSet(callback) { @@ -303,6 +319,10 @@ class Frontend { } const textSource = docRangeFromPoint(x, y, this.options); + return await this.searchSource(textSource, cause); + } + + async searchSource(textSource, cause) { let hideResults = textSource === null; let searched = false; let success = false; @@ -318,10 +338,10 @@ class Frontend { } catch (e) { if (window.yomichan_orphaned) { if (textSource && this.options.scanning.modifier !== 'none') { - this.popup.showOrphaned( + this.lastShowPromise = this.popup.showContent( textSource.getRect(), textSource.getWritingMode(), - this.options + 'orphaned' ); } } else { @@ -357,12 +377,11 @@ class Frontend { const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); const url = window.location.href; - this.popup.termsShow( + this.lastShowPromise = this.popup.showContent( textSource.getRect(), textSource.getWritingMode(), - definitions, - this.options, - {sentence, url, focus} + 'terms', + {definitions, context: {sentence, url, focus}} ); this.textSourceLast = textSource; @@ -388,12 +407,11 @@ class Frontend { const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); const url = window.location.href; - this.popup.kanjiShow( + this.lastShowPromise = this.popup.showContent( textSource.getRect(), textSource.getWritingMode(), - definitions, - this.options, - {sentence, url, focus} + 'kanji', + {definitions, context: {sentence, url, focus}} ); this.textSourceLast = textSource; @@ -558,8 +576,9 @@ Frontend.runtimeMessageHandlers = { popupSetVisibleOverride: (self, {visible}) => { self.popup.setVisibleOverride(visible); + }, + + getUrl: () => { + return {url: window.location.href}; } }; - - -window.yomichan_frontend = Frontend.create(); diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index b36de2ec..cec95aea 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -35,13 +35,14 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) { const ignoreNodes = options.scanning.enableOnPopupExpressions ? [] : [ '.expression', '.expression *' ]; - window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url}; + window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url, proxy: true}; const scriptSrcs = [ '/fg/js/frontend-api-sender.js', '/fg/js/popup.js', '/fg/js/popup-proxy.js', - '/fg/js/frontend.js' + '/fg/js/frontend.js', + '/fg/js/frontend-initialize.js' ]; for (const src of scriptSrcs) { const script = document.createElement('script'); diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index f933639c..d8dec4df 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -38,21 +38,23 @@ class PopupProxyHost { this.apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, { createNestedPopup: ({parentId}) => this.createNestedPopup(parentId), - show: ({id, elementRect, options}) => this.show(id, elementRect, options), - showOrphaned: ({id, elementRect, options}) => this.show(id, elementRect, options), + setOptions: ({id, options}) => this.setOptions(id, options), hide: ({id, changeFocus}) => this.hide(id, changeFocus), + isVisibleAsync: ({id}) => this.isVisibleAsync(id), setVisibleOverride: ({id, visible}) => this.setVisibleOverride(id, visible), containsPoint: ({id, x, y}) => this.containsPoint(id, x, y), - termsShow: ({id, elementRect, writingMode, definitions, options, context}) => this.termsShow(id, elementRect, writingMode, definitions, options, context), - kanjiShow: ({id, elementRect, writingMode, definitions, options, context}) => this.kanjiShow(id, elementRect, writingMode, definitions, options, context), + showContent: ({id, elementRect, writingMode, type, details}) => this.showContent(id, elementRect, writingMode, type, details), + setCustomCss: ({id, css}) => this.setCustomCss(id, css), clearAutoPlayTimer: ({id}) => this.clearAutoPlayTimer(id) }); } - createPopup(parentId) { + createPopup(parentId, depth) { const parent = (typeof parentId === 'string' && this.popups.hasOwnProperty(parentId) ? this.popups[parentId] : null); - const depth = (parent !== null ? parent.depth + 1 : 0); const id = `${this.nextId}`; + if (parent !== null) { + depth = parent.depth + 1; + } ++this.nextId; const popup = new Popup(id, depth, this.frameIdPromise); if (parent !== null) { @@ -64,7 +66,7 @@ class PopupProxyHost { } async createNestedPopup(parentId) { - return this.createPopup(parentId).id; + return this.createPopup(parentId, 0).id; } getPopup(id) { @@ -86,26 +88,24 @@ class PopupProxyHost { return new DOMRect(x, y, jsonRect.width, jsonRect.height); } - async show(id, elementRect, options) { + async setOptions(id, options) { const popup = this.getPopup(id); - elementRect = this.jsonRectToDOMRect(popup, elementRect); - return await popup.show(elementRect, options); + return await popup.setOptions(options); } - async showOrphaned(id, elementRect, options) { + async hide(id, changeFocus) { const popup = this.getPopup(id); - elementRect = this.jsonRectToDOMRect(popup, elementRect); - return await popup.showOrphaned(elementRect, options); + return popup.hide(changeFocus); } - async hide(id, changeFocus) { + async isVisibleAsync(id) { const popup = this.getPopup(id); - return popup.hide(changeFocus); + return await popup.isVisibleAsync(); } async setVisibleOverride(id, visible) { const popup = this.getPopup(id); - return popup.setVisibleOverride(visible); + return await popup.setVisibleOverride(visible); } async containsPoint(id, x, y) { @@ -113,18 +113,16 @@ class PopupProxyHost { return await popup.containsPoint(x, y); } - async termsShow(id, elementRect, writingMode, definitions, options, context) { + async showContent(id, elementRect, writingMode, type, details) { const popup = this.getPopup(id); elementRect = this.jsonRectToDOMRect(popup, elementRect); - if (!PopupProxyHost.popupCanShow(popup)) { return false; } - return await popup.termsShow(elementRect, writingMode, definitions, options, context); + if (!PopupProxyHost.popupCanShow(popup)) { return Promise.resolve(false); } + return await popup.showContent(elementRect, writingMode, type, details); } - async kanjiShow(id, elementRect, writingMode, definitions, options, context) { + async setCustomCss(id, css) { const popup = this.getPopup(id); - elementRect = this.jsonRectToDOMRect(popup, elementRect); - if (!PopupProxyHost.popupCanShow(popup)) { return false; } - return await popup.kanjiShow(elementRect, writingMode, definitions, options, context); + return popup.setCustomCss(css); } async clearAutoPlayTimer(id) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index efbd28b2..e62a4868 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -46,16 +46,9 @@ class PopupProxy { return id; } - async show(elementRect, options) { + async setOptions(options) { const id = await this.getPopupId(); - elementRect = PopupProxy.DOMRectToJson(elementRect); - return await this.invokeHostApi('show', {id, elementRect, options}); - } - - async showOrphaned(elementRect, options) { - const id = await this.getPopupId(); - elementRect = PopupProxy.DOMRectToJson(elementRect); - return await this.invokeHostApi('showOrphaned', {id, elementRect, options}); + return await this.invokeHostApi('setOptions', {id, options}); } async hide(changeFocus) { @@ -65,6 +58,11 @@ class PopupProxy { return await this.invokeHostApi('hide', {id: this.id, changeFocus}); } + async isVisibleAsync() { + const id = await this.getPopupId(); + return await this.invokeHostApi('isVisibleAsync', {id}); + } + async setVisibleOverride(visible) { const id = await this.getPopupId(); return await this.invokeHostApi('setVisibleOverride', {id, visible}); @@ -77,16 +75,15 @@ class PopupProxy { return await this.invokeHostApi('containsPoint', {id: this.id, x, y}); } - async termsShow(elementRect, writingMode, definitions, options, context) { + async showContent(elementRect, writingMode, type=null, details=null) { const id = await this.getPopupId(); elementRect = PopupProxy.DOMRectToJson(elementRect); - return await this.invokeHostApi('termsShow', {id, elementRect, writingMode, definitions, options, context}); + return await this.invokeHostApi('showContent', {id, elementRect, writingMode, type, details}); } - async kanjiShow(elementRect, writingMode, definitions, options, context) { + async setCustomCss(css) { const id = await this.getPopupId(); - elementRect = PopupProxy.DOMRectToJson(elementRect); - return await this.invokeHostApi('kanjiShow', {id, elementRect, writingMode, definitions, options, context}); + return await this.invokeHostApi('setCustomCss', {id, css}); } async clearAutoPlayTimer() { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 9ca91afa..b5eb9fe2 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -25,8 +25,9 @@ class Popup { this.frameId = null; this.parent = null; this.child = null; + this.childrenSupported = true; this.container = document.createElement('iframe'); - this.container.id = 'yomichan-float'; + this.container.className = 'yomichan-float'; this.container.addEventListener('mousedown', e => e.stopPropagation()); this.container.addEventListener('scroll', e => e.stopPropagation()); this.container.setAttribute('src', chrome.extension.getURL('/fg/float.html')); @@ -36,17 +37,19 @@ class Popup { this.isInjected = false; this.visible = false; this.visibleOverride = null; + this.options = null; + this.stylesheetInjectedViaApi = false; this.updateVisibility(); } - inject(options) { + inject() { if (this.injectPromise === null) { - this.injectPromise = this.createInjectPromise(options); + this.injectPromise = this.createInjectPromise(); } return this.injectPromise; } - async createInjectPromise(options) { + async createInjectPromise() { try { const {frameId} = await this.frameIdPromise; if (typeof frameId === 'number') { @@ -60,30 +63,44 @@ class Popup { const parentFrameId = (typeof this.frameId === 'number' ? this.frameId : null); this.container.addEventListener('load', () => { this.invokeApi('initialize', { - options: { - general: { - customPopupCss: options.general.customPopupCss - } - }, + options: this.options, popupInfo: { id: this.id, depth: this.depth, parentFrameId }, - url: this.url + url: this.url, + childrenSupported: this.childrenSupported }); resolve(); }); this.observeFullscreen(); this.onFullscreenChanged(); + this.setCustomOuterCss(this.options.general.customPopupOuterCss, false); this.isInjected = true; }); } - async show(elementRect, writingMode, options) { - await this.inject(options); + isInitialized() { + return this.options !== null; + } + + async setOptions(options) { + this.options = options; + this.updateTheme(); + } + + async showContent(elementRect, writingMode, type=null, details=null) { + if (!this.isInitialized()) { return; } + await this.show(elementRect, writingMode); + if (type === null) { return; } + this.invokeApi('setContent', {type, details}); + } - const optionsGeneral = options.general; + async show(elementRect, writingMode) { + await this.inject(); + + const optionsGeneral = this.options.general; const container = this.container; const containerRect = container.getBoundingClientRect(); const getPosition = ( @@ -208,11 +225,6 @@ class Popup { return [position, size, after]; } - async showOrphaned(elementRect, writingMode, options) { - await this.show(elementRect, writingMode, options); - this.invokeApi('orphaned'); - } - hide(changeFocus) { if (!this.isVisible()) { return; @@ -227,6 +239,10 @@ class Popup { } } + async isVisibleAsync() { + return this.isVisible(); + } + isVisible() { return this.isInjected && (this.visibleOverride !== null ? this.visibleOverride : this.visible); } @@ -261,6 +277,44 @@ class Popup { } } + updateTheme() { + this.container.dataset.yomichanTheme = this.options.general.popupOuterTheme; + this.container.dataset.yomichanSiteColor = this.getSiteColor(); + } + + getSiteColor() { + const color = [255, 255, 255]; + Popup.addColor(color, Popup.getColorInfo(window.getComputedStyle(document.documentElement).backgroundColor)); + Popup.addColor(color, Popup.getColorInfo(window.getComputedStyle(document.body).backgroundColor)); + const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128); + return dark ? 'dark' : 'light'; + } + + static addColor(target, color) { + if (color === null) { return; } + + const a = color[3]; + if (a <= 0.0) { return; } + + const aInv = 1.0 - a; + for (let i = 0; i < 3; ++i) { + target[i] = target[i] * aInv + color[i] * a; + } + } + + static getColorInfo(cssColor) { + const m = /^\s*rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d\.]+)\s*)?\)\s*$/.exec(cssColor); + if (m === null) { return null; } + + const m4 = m[4]; + return [ + Number.parseInt(m[1], 10), + Number.parseInt(m[2], 10), + Number.parseInt(m[3], 10), + m4 ? Math.max(0.0, Math.min(1.0, Number.parseFloat(m4))) : 1.0 + ]; + } + async containsPoint(x, y) { for (let popup = this; popup !== null && popup.isVisible(); popup = popup.child) { const rect = popup.container.getBoundingClientRect(); @@ -271,14 +325,25 @@ class Popup { return false; } - async termsShow(elementRect, writingMode, definitions, options, context) { - await this.show(elementRect, writingMode, options); - this.invokeApi('termsShow', {definitions, options, context}); + async setCustomCss(css) { + this.invokeApi('setCustomCss', {css}); } - async kanjiShow(elementRect, writingMode, definitions, options, context) { - await this.show(elementRect, writingMode, options); - this.invokeApi('kanjiShow', {definitions, options, context}); + async setCustomOuterCss(css, injectDirectly) { + // Cannot repeatedly inject stylesheets using web extension APIs since there is no way to remove them. + if (this.stylesheetInjectedViaApi) { return; } + + if (injectDirectly || Popup.isOnExtensionPage()) { + Popup.injectOuterStylesheet(css); + } else { + if (!css) { return; } + try { + await apiInjectStylesheet(css); + this.stylesheetInjectedViaApi = true; + } catch (e) { + // NOP + } + } } clearAutoPlayTimer() { @@ -322,4 +387,35 @@ class Popup { get url() { return window.location.href; } + + static isOnExtensionPage() { + try { + const url = chrome.runtime.getURL('/'); + return window.location.href.substr(0, url.length) === url; + } catch (e) { + // NOP + } + } + + static injectOuterStylesheet(css) { + if (Popup.outerStylesheet === null) { + if (!css) { return; } + Popup.outerStylesheet = document.createElement('style'); + Popup.outerStylesheet.id = "yomichan-popup-outer-stylesheet"; + } + + const outerStylesheet = Popup.outerStylesheet; + if (css) { + outerStylesheet.textContent = css; + + const par = document.head; + if (par && outerStylesheet.parentNode !== par) { + par.appendChild(outerStylesheet); + } + } else { + outerStylesheet.textContent = ''; + } + } } + +Popup.outerStylesheet = null; diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index ee4f58e2..c3da9f46 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -229,13 +229,29 @@ class TextSourceRange { } static getElementWritingMode(element) { - if (element === null) { - return 'horizontal-tb'; + if (element !== null) { + const style = window.getComputedStyle(element); + const writingMode = style.writingMode; + if (typeof writingMode === 'string') { + return TextSourceRange.normalizeWritingMode(writingMode); + } } + return 'horizontal-tb'; + } - const style = window.getComputedStyle(element); - const writingMode = style.writingMode; - return typeof writingMode === 'string' ? writingMode : 'horizontal-tb'; + 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; + } } static getNodesInRange(range) { |