diff options
author | Alex Yatskov <alex@foosoft.net> | 2017-08-17 19:39:32 -0700 |
---|---|---|
committer | Alex Yatskov <alex@foosoft.net> | 2017-08-17 19:39:32 -0700 |
commit | 39fbabfe626e7ae2c2e0c1dd45bda33dd5dab66c (patch) | |
tree | 334bc2282389c910ed9f7d724458b6d8b650311a /ext/fg/js | |
parent | 65c100fe16c4829971055da384da90b1a1cddcd5 (diff) | |
parent | 7586572fbaab7de698ec13f8712cc95e24ab6273 (diff) |
Merge branch 'master' into firefox-amo
Diffstat (limited to 'ext/fg/js')
-rw-r--r-- | ext/fg/js/api.js | 58 | ||||
-rw-r--r-- | ext/fg/js/document.js | 163 | ||||
-rw-r--r-- | ext/fg/js/float.js (renamed from ext/fg/js/display-frame.js) | 64 | ||||
-rw-r--r-- | ext/fg/js/frontend.js (renamed from ext/fg/js/driver.js) | 230 | ||||
-rw-r--r-- | ext/fg/js/popup.js | 111 | ||||
-rw-r--r-- | ext/fg/js/source-element.js | 77 | ||||
-rw-r--r-- | ext/fg/js/source.js (renamed from ext/fg/js/source-range.js) | 70 | ||||
-rw-r--r-- | ext/fg/js/util.js | 191 |
8 files changed, 504 insertions, 460 deletions
diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js new file mode 100644 index 00000000..151882eb --- /dev/null +++ b/ext/fg/js/api.js @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016-2017 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/>. + */ + + +function apiOptionsSet(options) { + return utilInvoke('optionsSet', {options}); +} + +function apiOptionsGet() { + return utilInvoke('optionsGet'); +} + +function apiTermsFind(text) { + return utilInvoke('termsFind', {text}); +} + +function apiKanjiFind(text) { + return utilInvoke('kanjiFind', {text}); +} + +function apiDefinitionAdd(definition, mode) { + return utilInvoke('definitionAdd', {definition, mode}); +} + +function apiDefinitionsAddable(definitions, modes) { + return utilInvoke('definitionsAddable', {definitions, modes}).catch(() => null); +} + +function apiNoteView(noteId) { + return utilInvoke('noteView', {noteId}); +} + +function apiTemplateRender(template, data) { + return utilInvoke('templateRender', {data, template}); +} + +function apiCommandExec(command) { + return utilInvoke('commandExec', {command}); +} + +function apiAudioGetUrl(definition, source) { + return utilInvoke('audioGetUrl', {definition, source}); +} diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js new file mode 100644 index 00000000..26c85b40 --- /dev/null +++ b/ext/fg/js/document.js @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2016-2017 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/>. + */ + + +function docOffsetCalc(element) { + const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft; + + const clientTop = document.documentElement.clientTop || document.body.clientTop || 0; + const clientLeft = document.documentElement.clientLeft || document.body.clientLeft || 0; + + const rect = element.getBoundingClientRect(); + const top = Math.round(rect.top + scrollTop - clientTop); + const left = Math.round(rect.left + scrollLeft - clientLeft); + + return {top, left}; +} + +function docImposterCreate(element) { + const styleProps = window.getComputedStyle(element); + const stylePairs = []; + for (const key of styleProps) { + stylePairs.push(`${key}: ${styleProps[key]};`); + } + + const offset = docOffsetCalc(element); + const imposter = document.createElement('div'); + imposter.className = 'yomichan-imposter'; + imposter.innerText = element.value; + imposter.style.cssText = stylePairs.join('\n'); + imposter.style.position = 'absolute'; + imposter.style.top = `${offset.top}px`; + imposter.style.left = `${offset.left}px`; + imposter.style.opacity = 0; + imposter.style.zIndex = 2147483646; + if (element.nodeName === 'TEXTAREA' && styleProps.overflow === 'visible') { + imposter.style.overflow = 'auto'; + } + + document.body.appendChild(imposter); + imposter.scrollTop = element.scrollTop; + imposter.scrollLeft = element.scrollLeft; +} + +function docImposterDestroy() { + for (const element of document.getElementsByClassName('yomichan-imposter')) { + element.parentNode.removeChild(element); + } +} + +function docRangeFromPoint(point) { + const element = document.elementFromPoint(point.x, point.y); + if (element) { + if (element.nodeName === 'IMG' || element.nodeName === 'BUTTON') { + return new TextSourceElement(element); + } else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { + docImposterCreate(element); + } + } + + if (!document.caretRangeFromPoint) { + document.caretRangeFromPoint = (x, y) => { + const position = document.caretPositionFromPoint(x,y); + if (position) { + const range = document.createRange(); + range.setStart(position.offsetNode, position.offset); + range.setEnd(position.offsetNode, position.offset); + return range; + } + }; + } + + const range = document.caretRangeFromPoint(point.x, point.y); + if (range) { + return new TextSourceRange(range); + } +} + +function docSentenceExtract(source, extent) { + const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; + const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; + const terminators = '…。..??!!'; + + const sourceLocal = source.clone(); + const position = sourceLocal.setStartOffset(extent); + sourceLocal.setEndOffset(position + extent); + 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 = [quotesBwd[c]].concat(quoteStack); + } + } + + 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 = [quotesFwd[c]].concat(quoteStack); + } + } + + const text = content.substring(startPos, endPos); + const padding = text.length - text.replace(/^\s+/, '').length; + + return { + text: text.trim(), + offset: position - startPos - padding + }; +} diff --git a/ext/fg/js/display-frame.js b/ext/fg/js/float.js index 9fd09e74..22374f8b 100644 --- a/ext/fg/js/display-frame.js +++ b/ext/fg/js/float.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -17,61 +17,45 @@ */ -window.displayFrame = new class extends Display { +class DisplayFloat extends Display { constructor() { - super($('#spinner'), $('#content')); - $(window).on('message', this.onMessage.bind(this)); + super($('#spinner'), $('#definitions')); + $(window).on('message', utilAsync(this.onMessage.bind(this))); } - definitionAdd(definition, mode) { - return bgDefinitionAdd(definition, mode); - } - - definitionsAddable(definitions, modes) { - return bgDefinitionsAddable(definitions, modes); - } - - templateRender(template, data) { - return bgTemplateRender(template, data); - } - - kanjiFind(character) { - return bgKanjiFind(character); - } - - handleError(error) { - if (window.orphaned) { - this.showOrphaned(); + onError(error) { + if (window.yomichan_orphaned) { + this.onOrphaned(); } else { window.alert(`Error: ${error}`); } } - clearSearch() { - window.parent.postMessage('popupClose', '*'); + onOrphaned() { + $('#definitions').hide(); + $('#error-orphaned').show(); } - selectionCopy() { - window.parent.postMessage('selectionCopy', '*'); + onSearchClear() { + window.parent.postMessage('popupClose', '*'); } - showOrphaned() { - $('#content').hide(); - $('#orphan').show(); + onSelectionCopy() { + window.parent.postMessage('selectionCopy', '*'); } onMessage(e) { const handlers = { - showTermDefs: ({definitions, options, context}) => { - this.showTermDefs(definitions, options, context); + termsShow: ({definitions, options, context}) => { + this.termsShow(definitions, options, context); }, - showKanjiDefs: ({definitions, options, context}) => { - this.showKanjiDefs(definitions, options, context); + kanjiShow: ({definitions, options, context}) => { + this.kanjiShow(definitions, options, context); }, - showOrphaned: () => { - this.showOrphaned(); + orphaned: () => { + this.onOrphaned(); } }; @@ -85,8 +69,8 @@ window.displayFrame = new class extends Display { onKeyDown(e) { const handlers = { 67: /* c */ () => { - if (e.ctrlKey && window.getSelection().toString() === '') { - this.selectionCopy(); + if (e.ctrlKey && !window.getSelection().toString()) { + this.onSelectionCopy(); return true; } } @@ -99,4 +83,6 @@ window.displayFrame = new class extends Display { super.onKeyDown(e); } } -}; +} + +window.yomichan_display = new DisplayFloat(); diff --git a/ext/fg/js/driver.js b/ext/fg/js/frontend.js index b0cc4613..41c93f00 100644 --- a/ext/fg/js/driver.js +++ b/ext/fg/js/frontend.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -17,38 +17,31 @@ */ -window.driver = new class { +class Frontend { constructor() { this.popup = new Popup(); this.popupTimer = null; - this.lastMousePos = null; this.mouseDownLeft = false; this.mouseDownMiddle = false; - this.lastTextSource = null; + this.textSourceLast = null; this.pendingLookup = false; this.options = null; + } - bgOptionsGet().then(options => { - this.options = options; - window.addEventListener('mouseover', this.onMouseOver.bind(this)); + async prepare() { + try { + this.options = await apiOptionsGet(); + + window.addEventListener('message', this.onFrameMessage.bind(this)); window.addEventListener('mousedown', this.onMouseDown.bind(this)); - window.addEventListener('mouseup', this.onMouseUp.bind(this)); window.addEventListener('mousemove', this.onMouseMove.bind(this)); - window.addEventListener('resize', e => this.searchClear()); - window.addEventListener('message', this.onFrameMessage.bind(this)); - chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this)); - }).catch(this.handleError.bind(this)); - } - - popupTimerSet(callback) { - this.popupTimerClear(); - this.popupTimer = window.setTimeout(callback, this.options.scanning.delay); - } + window.addEventListener('mouseover', this.onMouseOver.bind(this)); + window.addEventListener('mouseup', this.onMouseUp.bind(this)); + window.addEventListener('resize', this.onResize.bind(this)); - popupTimerClear() { - if (this.popupTimer) { - window.clearTimeout(this.popupTimer); - this.popupTimer = null; + chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this)); + } catch (e) { + this.onError(e); } } @@ -59,7 +52,6 @@ window.driver = new class { } onMouseMove(e) { - this.lastMousePos = {x: e.clientX, y: e.clientY}; this.popupTimerClear(); if (!this.options.general.enable) { @@ -70,6 +62,10 @@ window.driver = new class { return; } + if (this.pendingLookup) { + return; + } + const mouseScan = this.mouseDownMiddle && this.options.scanning.middleMouse; const keyScan = this.options.scanning.modifier === 'alt' && e.altKey || @@ -81,16 +77,24 @@ window.driver = new class { return; } - const searchFunc = () => this.searchAt(this.lastMousePos); + const search = async () => { + try { + await this.searchAt({x: e.clientX, y: e.clientY}); + this.pendingLookup = false; + } catch (e) { + this.onError(e); + } + }; + if (this.options.scanning.modifier === 'none') { - this.popupTimerSet(searchFunc); + this.popupTimerSet(search); } else { - searchFunc(); + search(); } } onMouseDown(e) { - this.lastMousePos = {x: e.clientX, y: e.clientY}; + this.mousePosLast = {x: e.clientX, y: e.clientY}; this.popupTimerClear(); this.searchClear(); @@ -126,6 +130,10 @@ window.driver = new class { } } + onResize() { + this.searchClear(); + } + onBgMessage({action, params}, sender, callback) { const handlers = { optionsSet: options => { @@ -144,106 +152,122 @@ window.driver = new class { callback(); } - searchAt(point) { - if (this.pendingLookup) { - return; - } + onError(error) { + window.alert(`Error: ${error}`); + } - const textSource = docRangeFromPoint(point); - if (!textSource || !textSource.containsPoint(point)) { - docImposterDestroy(); - return; - } + popupTimerSet(callback) { + this.popupTimerClear(); + this.popupTimer = window.setTimeout(callback, this.options.scanning.delay); + } - if (this.lastTextSource && this.lastTextSource.equals(textSource)) { - return; + popupTimerClear() { + if (this.popupTimer) { + window.clearTimeout(this.popupTimer); + this.popupTimer = null; } + } - this.pendingLookup = true; - this.searchTerms(textSource).then(found => { - if (!found) { - return this.searchKanji(textSource); + async searchAt(point) { + let textSource = null; + + try { + if (this.pendingLookup) { + return; + } + + textSource = docRangeFromPoint(point); + if (!textSource || !textSource.containsPoint(point)) { + docImposterDestroy(); + return; + } + + if (this.textSourceLast && this.textSourceLast.equals(textSource)) { + return; + } + + this.pendingLookup = true; + + if (!await this.searchTerms(textSource)) { + await this.searchKanji(textSource); + } + } catch (e) { + if (window.yomichan_orphaned) { + if (textSource && this.options.scanning.modifier !== 'none') { + this.popup.showOrphaned(textSource.getRect(), this.options); + } + } else { + this.onError(e); } - }).catch(error => { - this.handleError(error, textSource); - }).then(() => { + } finally { docImposterDestroy(); this.pendingLookup = false; - }); + } } - searchTerms(textSource) { + async searchTerms(textSource) { textSource.setEndOffset(this.options.scanning.length); - return bgTermsFind(textSource.text()).then(({definitions, length}) => { - if (definitions.length === 0) { - return false; - } else { - textSource.setEndOffset(length); - - const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); - const url = window.location.href; - this.popup.showTermDefs( - textSource.getRect(), - definitions, - this.options, - {sentence, url} - ); - - this.lastTextSource = textSource; - if (this.options.scanning.selectText) { - textSource.select(); - } + const {definitions, length} = await apiTermsFind(textSource.text()); + if (definitions.length === 0) { + return false; + } - return true; - } - }); + textSource.setEndOffset(length); + + const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); + const url = window.location.href; + this.popup.termsShow( + textSource.getRect(), + definitions, + this.options, + {sentence, url} + ); + + this.textSourceLast = textSource; + if (this.options.scanning.selectText) { + textSource.select(); + } + + return true; } - searchKanji(textSource) { + async searchKanji(textSource) { textSource.setEndOffset(1); - return bgKanjiFind(textSource.text()).then(definitions => { - if (definitions.length === 0) { - return false; - } else { - const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); - const url = window.location.href; - this.popup.showKanjiDefs( - textSource.getRect(), - definitions, - this.options, - {sentence, url} - ); - - this.lastTextSource = textSource; - if (this.options.scanning.selectText) { - textSource.select(); - } + const definitions = await apiKanjiFind(textSource.text()); + if (definitions.length === 0) { + return false; + } - return true; - } - }); + const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); + const url = window.location.href; + this.popup.kanjiShow( + textSource.getRect(), + definitions, + this.options, + {sentence, url} + ); + + this.textSourceLast = textSource; + if (this.options.scanning.selectText) { + textSource.select(); + } + + return true; } searchClear() { docImposterDestroy(); this.popup.hide(); - if (this.options.scanning.selectText && this.lastTextSource) { - this.lastTextSource.deselect(); + if (this.options.scanning.selectText && this.textSourceLast) { + this.textSourceLast.deselect(); } - this.lastTextSource = null; + this.textSourceLast = null; } +} - handleError(error, textSource) { - if (window.orphaned) { - if (textSource && this.options.scanning.modifier !== 'none') { - this.popup.showOrphaned(textSource.getRect(), this.options); - } - } else { - window.alert(`Error: ${error}`); - } - } -}; +window.yomichan_frontend = new Frontend(); +window.yomichan_frontend.prepare(); diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index cd7e846a..03958832 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -20,10 +20,10 @@ class Popup { constructor() { this.container = document.createElement('iframe'); - this.container.id = 'yomichan-popup'; + this.container.id = 'yomichan-float'; this.container.addEventListener('mousedown', e => e.stopPropagation()); this.container.addEventListener('scroll', e => e.stopPropagation()); - this.container.setAttribute('src', chrome.extension.getURL('/fg/frame.html')); + this.container.setAttribute('src', chrome.extension.getURL('/fg/float.html')); this.container.style.width = '0px'; this.container.style.height = '0px'; this.injected = null; @@ -40,51 +40,56 @@ class Popup { return this.injected; } - show(elementRect, options) { - return this.inject().then(() => { - const containerStyle = window.getComputedStyle(this.container); - const containerHeight = parseInt(containerStyle.height); - const containerWidth = parseInt(containerStyle.width); - - const limitX = document.body.clientWidth; - const limitY = window.innerHeight; - - let x = elementRect.left; - let width = Math.max(containerWidth, options.general.popupWidth); - const overflowX = Math.max(x + width - limitX, 0); - if (overflowX > 0) { - if (x >= overflowX) { - x -= overflowX; - } else { - width = limitX; - x = 0; - } - } + async show(elementRect, options) { + await this.inject(); + + const containerStyle = window.getComputedStyle(this.container); + const containerHeight = parseInt(containerStyle.height); + const containerWidth = parseInt(containerStyle.width); + + const limitX = document.body.clientWidth; + const limitY = window.innerHeight; - let y = 0; - let height = Math.max(containerHeight, options.general.popupHeight); - const yBelow = elementRect.bottom + options.general.popupOffset; - const yAbove = elementRect.top - options.general.popupOffset; - const overflowBelow = Math.max(yBelow + height - limitY, 0); - const overflowAbove = Math.max(height - yAbove, 0); - if (overflowBelow > 0 || overflowAbove > 0) { - if (overflowBelow < overflowAbove) { - height = Math.max(height - overflowBelow, 0); - y = yBelow; - } else { - height = Math.max(height - overflowAbove, 0); - y = Math.max(yAbove - height, 0); - } + let x = elementRect.left; + let width = Math.max(containerWidth, options.general.popupWidth); + const overflowX = Math.max(x + width - limitX, 0); + if (overflowX > 0) { + if (x >= overflowX) { + x -= overflowX; } else { + width = limitX; + x = 0; + } + } + + let y = 0; + let height = Math.max(containerHeight, options.general.popupHeight); + const yBelow = elementRect.bottom + options.general.popupOffset; + const yAbove = elementRect.top - options.general.popupOffset; + const overflowBelow = Math.max(yBelow + height - limitY, 0); + const overflowAbove = Math.max(height - yAbove, 0); + if (overflowBelow > 0 || overflowAbove > 0) { + if (overflowBelow < overflowAbove) { + height = Math.max(height - overflowBelow, 0); y = yBelow; + } else { + height = Math.max(height - overflowAbove, 0); + y = Math.max(yAbove - height, 0); } + } else { + y = yBelow; + } + + this.container.style.left = `${x}px`; + this.container.style.top = `${y}px`; + this.container.style.width = `${width}px`; + this.container.style.height = `${height}px`; + this.container.style.visibility = 'visible'; + } - this.container.style.left = `${x}px`; - this.container.style.top = `${y}px`; - this.container.style.width = `${width}px`; - this.container.style.height = `${height}px`; - this.container.style.visibility = 'visible'; - }); + async showOrphaned(elementRect, options) { + await this.show(elementRect, options); + this.invokeApi('orphaned'); } hide() { @@ -95,22 +100,14 @@ class Popup { return this.injected && this.container.style.visibility !== 'hidden'; } - showTermDefs(elementRect, definitions, options, context) { - this.show(elementRect, options).then(() => { - this.invokeApi('showTermDefs', {definitions, options, context}); - }); - } - - showKanjiDefs(elementRect, definitions, options, context) { - this.show(elementRect, options).then(() => { - this.invokeApi('showKanjiDefs', {definitions, options, context}); - }); + async termsShow(elementRect, definitions, options, context) { + await this.show(elementRect, options); + this.invokeApi('termsShow', {definitions, options, context}); } - showOrphaned(elementRect, options) { - this.show(elementRect, options).then(() => { - this.invokeApi('showOrphaned'); - }); + async kanjiShow(elementRect, definitions, options, context) { + await this.show(elementRect, options); + this.invokeApi('kanjiShow', {definitions, options, context}); } invokeApi(action, params={}) { diff --git a/ext/fg/js/source-element.js b/ext/fg/js/source-element.js deleted file mode 100644 index a8101382..00000000 --- a/ext/fg/js/source-element.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2016 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/>. - */ - - -class TextSourceElement { - constructor(element, content='') { - this.element = element; - this.content = content; - } - - clone() { - return new TextSourceElement(this.element, this.content); - } - - text() { - return this.content; - } - - setEndOffset(length) { - switch (this.element.nodeName) { - case 'BUTTON': - this.content = this.element.innerHTML; - break; - case 'IMG': - this.content = this.element.getAttribute('alt'); - break; - default: - this.content = this.element.value; - break; - } - - this.content = this.content || ''; - this.content = this.content.substring(0, length); - - return this.content.length; - } - - setStartOffset(length) { - return 0; - } - - containsPoint(point) { - const rect = this.getRect(); - return point.x >= rect.left && point.x <= rect.right; - } - - getRect() { - return this.element.getBoundingClientRect(); - } - - select() { - // NOP - } - - deselect() { - // NOP - } - - equals(other) { - return other.element === this.element && other.content === this.content; - } -} diff --git a/ext/fg/js/source-range.js b/ext/fg/js/source.js index fa73b0a4..3b6ecb2a 100644 --- a/ext/fg/js/source-range.js +++ b/ext/fg/js/source.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -17,6 +17,10 @@ */ +/* + * TextSourceRange + */ + class TextSourceRange { constructor(range, content='') { this.range = range; @@ -174,3 +178,67 @@ class TextSourceRange { return state.remainder > 0; } } + + +/* + * TextSourceElement + */ + +class TextSourceElement { + constructor(element, content='') { + this.element = element; + this.content = content; + } + + clone() { + return new TextSourceElement(this.element, this.content); + } + + text() { + return this.content; + } + + setEndOffset(length) { + switch (this.element.nodeName) { + case 'BUTTON': + this.content = this.element.innerHTML; + break; + case 'IMG': + this.content = this.element.getAttribute('alt'); + break; + default: + this.content = this.element.value; + break; + } + + this.content = this.content || ''; + this.content = this.content.substring(0, length); + + return this.content.length; + } + + setStartOffset(length) { + return 0; + } + + containsPoint(point) { + const rect = this.getRect(); + return point.x >= rect.left && point.x <= rect.right; + } + + getRect() { + return this.element.getBoundingClientRect(); + } + + select() { + // NOP + } + + deselect() { + // NOP + } + + equals(other) { + return other.element === this.element && other.content === this.content; + } +} diff --git a/ext/fg/js/util.js b/ext/fg/js/util.js index c6270ce6..5eff4018 100644 --- a/ext/fg/js/util.js +++ b/ext/fg/js/util.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -17,11 +17,13 @@ */ -/* - * Background - */ +function utilAsync(func) { + return function(...args) { + func.apply(this, args); + }; +} -function bgInvoke(action, params) { +function utilInvoke(action, params={}) { return new Promise((resolve, reject) => { try { chrome.runtime.sendMessage({action, params}, ({result, error}) => { @@ -32,185 +34,8 @@ function bgInvoke(action, params) { } }); } catch (e) { - window.orphaned = true; + window.yomichan_orphaned = true; reject(e.message); } }); } - -function bgOptionsGet() { - return bgInvoke('optionsGet', {}); -} - -function bgTermsFind(text) { - return bgInvoke('termsFind', {text}); -} - -function bgKanjiFind(text) { - return bgInvoke('kanjiFind', {text}); -} - -function bgTemplateRender(template, data) { - return bgInvoke('templateRender', {data, template}); -} - -function bgDefinitionsAddable(definitions, modes) { - return bgInvoke('definitionsAddable', {definitions, modes}).catch(() => null); -} - -function bgDefinitionAdd(definition, mode) { - return bgInvoke('definitionAdd', {definition, mode}); -} - - -/* - * Document - */ - -function docOffsetCalc(element) { - const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; - const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft; - - const clientTop = document.documentElement.clientTop || document.body.clientTop || 0; - const clientLeft = document.documentElement.clientLeft || document.body.clientLeft || 0; - - const rect = element.getBoundingClientRect(); - const top = Math.round(rect.top + scrollTop - clientTop); - const left = Math.round(rect.left + scrollLeft - clientLeft); - - return {top, left}; -} - -function docImposterCreate(element) { - const styleProps = window.getComputedStyle(element); - const stylePairs = []; - for (const key of styleProps) { - stylePairs.push(`${key}: ${styleProps[key]};`); - } - - const offset = docOffsetCalc(element); - const imposter = document.createElement('div'); - imposter.className = 'yomichan-imposter'; - imposter.innerText = element.value; - imposter.style.cssText = stylePairs.join('\n'); - imposter.style.position = 'absolute'; - imposter.style.top = `${offset.top}px`; - imposter.style.left = `${offset.left}px`; - imposter.style.zIndex = 2147483646; - if (element.nodeName === 'TEXTAREA' && styleProps.overflow === 'visible') { - imposter.style.overflow = 'auto'; - } - - document.body.appendChild(imposter); - imposter.scrollTop = element.scrollTop; - imposter.scrollLeft = element.scrollLeft; -} - -function docImposterDestroy() { - for (const element of document.getElementsByClassName('yomichan-imposter')) { - element.parentNode.removeChild(element); - } -} - -function docRangeFromPoint(point) { - const element = document.elementFromPoint(point.x, point.y); - if (element !== null) { - if (element.nodeName === 'IMG' || element.nodeName === 'BUTTON') { - return new TextSourceElement(element); - } else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { - docImposterCreate(element); - } - } - - if (!document.caretRangeFromPoint) { - document.caretRangeFromPoint = (x, y) => { - const position = document.caretPositionFromPoint(x,y); - if (position === null) { - return null; - } - - const range = document.createRange(); - range.setStart(position.offsetNode, position.offset); - range.setEnd(position.offsetNode, position.offset); - return range; - }; - } - - const range = document.caretRangeFromPoint(point.x, point.y); - if (range !== null) { - return new TextSourceRange(range); - } - - return null; -} - -function docSentenceExtract(source, extent) { - const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; - const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; - const terminators = '…。..??!!'; - - const sourceLocal = source.clone(); - const position = sourceLocal.setStartOffset(extent); - sourceLocal.setEndOffset(position + extent); - 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 = [quotesBwd[c]].concat(quoteStack); - } - } - - 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 = [quotesFwd[c]].concat(quoteStack); - } - } - - const text = content.substring(startPos, endPos); - const padding = text.length - text.replace(/^\s+/, '').length; - - return { - text: text.trim(), - offset: position - startPos - padding - }; -} |