diff options
Diffstat (limited to 'ext/fg/js')
-rw-r--r-- | ext/fg/js/driver.js | 228 | ||||
-rw-r--r-- | ext/fg/js/frame.js | 168 | ||||
-rw-r--r-- | ext/fg/js/popup.js | 63 | ||||
-rw-r--r-- | ext/fg/js/util.js | 22 |
4 files changed, 216 insertions, 265 deletions
diff --git a/ext/fg/js/driver.js b/ext/fg/js/driver.js index 9aab7950..ef7db481 100644 --- a/ext/fg/js/driver.js +++ b/ext/fg/js/driver.js @@ -21,27 +21,22 @@ class Driver { constructor() { this.popup = new Popup(); this.popupTimer = null; - this.audio = {}; this.lastMousePos = null; this.lastTextSource = null; this.pendingLookup = false; this.enabled = false; this.options = null; - this.definitions = null; - this.sequence = 0; - this.fgRoot = chrome.extension.getURL('fg'); chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this)); - window.addEventListener('message', this.onFrameMessage.bind(this)); window.addEventListener('mouseover', this.onMouseOver.bind(this)); window.addEventListener('mousedown', this.onMouseDown.bind(this)); window.addEventListener('mousemove', this.onMouseMove.bind(this)); window.addEventListener('keydown', this.onKeyDown.bind(this)); - window.addEventListener('resize', e => this.hidePopup()); + window.addEventListener('resize', e => this.searchClear()); getOptions().then(opts => { this.options = opts; - return getEnabled(); + return isEnabled(); }).then(enabled => { this.enabled = enabled; }); @@ -65,7 +60,7 @@ class Driver { if (this.enabled && this.lastMousePos !== null && e.keyCode === 16 /* shift */) { this.searchAt(this.lastMousePos, true); } else if (e.keyCode === 27 /* esc */) { - this.hidePopup(); + this.searchClear(); } } @@ -92,7 +87,7 @@ class Driver { } const searcher = () => this.searchAt(this.lastMousePos, false); - if (!this.popup.visible() || e.shiftKey || e.which === 2 /* mmb */) { + if (!this.popup.isVisible() || e.shiftKey || e.which === 2 /* mmb */) { searcher(); } else { this.popupTimerSet(searcher); @@ -102,7 +97,7 @@ class Driver { onMouseDown(e) { this.lastMousePos = {x: e.clientX, y: e.clientY}; this.popupTimerClear(); - this.hidePopup(); + this.searchClear(); } onBgMessage({action, params}, sender, callback) { @@ -114,20 +109,46 @@ class Driver { callback(); } - onFrameMessage(e) { - const {action, params} = e.data, method = this['api_' + action]; - if (typeof(method) === 'function') { - method.call(this, params); + searchAt(point, hideNotFound) { + if (this.pendingLookup) { + return; + } + + const textSource = textSourceFromPoint(point); + if (textSource === null || !textSource.containsPoint(point)) { + if (hideNotFound) { + this.searchClear(); + } + + return; + } + + if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) { + return; } + + this.pendingLookup = true; + this.searchTerms(textSource).then(found => { + if (!found) { + this.searchKanji(textSource).then(found => { + if (!found && hideNotFound) { + this.searchClear(); + } + }); + } + }).catch(error => { + window.alert('Error: ' + error); + }).then(() => { + this.pendingLookup = false; + }); } searchTerms(textSource) { textSource.setEndOffset(this.options.scanLength); - this.pendingLookup = true; - return findTerm(textSource.text()).then(({definitions, length}) => { + const findFunc = this.options.groupTermResults ? findTermGrouped : findTerm; + return findFunc(textSource.text()).then(({definitions, length}) => { if (definitions.length === 0) { - this.pendingLookup = false; return false; } else { textSource.setEndOffset(length); @@ -138,119 +159,46 @@ class Driver { definition.sentence = sentence; }); - const sequence = ++this.sequence; - const context = { - definitions, - sequence, - addable: this.options.ankiMethod !== 'disabled', - root: this.fgRoot, - options: this.options - }; - - return renderText(context, 'term-list.html').then(content => { - this.definitions = definitions; - this.pendingLookup = false; - this.showPopup(textSource, content); - return canAddDefinitions(definitions, ['term_kanji', 'term_kana']); - }).then(states => { - if (states !== null) { - states.forEach((state, index) => this.popup.invokeApi( - 'setActionState', - {index, state, sequence} - )); - } + this.popup.showNextTo(textSource.getRect()); + this.popup.showTermDefs(definitions, this.options); + this.lastTextSource = textSource; + if (this.options.selectMatchedText) { + textSource.select(); + } - return true; - }); + return true; } }).catch(error => { - alert('Error: ' + error); + window.alert('Error: ' + error); + return false; }); } searchKanji(textSource) { textSource.setEndOffset(1); - this.pendingLookup = true; return findKanji(textSource.text()).then(definitions => { if (definitions.length === 0) { - this.pendingLookup = false; return false; } else { definitions.forEach(definition => definition.url = window.location.href); - const sequence = ++this.sequence; - const context = { - definitions, - sequence, - addable: this.options.ankiMethod !== 'disabled', - root: this.fgRoot, - options: this.options - }; - - return renderText(context, 'kanji-list.html').then(content => { - this.definitions = definitions; - this.pendingLookup = false; - this.showPopup(textSource, content); - return canAddDefinitions(definitions, ['kanji']); - }).then(states => { - if (states !== null) { - states.forEach((state, index) => this.popup.invokeApi( - 'setActionState', - {index, state, sequence} - )); - } - - return true; - }); - } - }).catch(error => { - alert('Error: ' + error); - }); - } - - searchAt(point, hideNotFound) { - if (this.pendingLookup) { - return; - } - - const textSource = textSourceFromPoint(point); - if (textSource === null || !textSource.containsPoint(point)) { - if (hideNotFound) { - this.hidePopup(); - } - - return; - } - - if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) { - return true; - } + this.popup.showNextTo(textSource.getRect()); + this.popup.showKanjiDefs(definitions, this.options); + this.lastTextSource = textSource; + if (this.options.selectMatchedText) { + textSource.select(); + } - this.searchTerms(textSource).then(found => { - if (!found) { - this.searchKanji(textSource).then(found => { - if (!found && hideNotFound) { - this.hidePopup(); - } - }); + return true; } }).catch(error => { - alert('Error: ' + error); + window.alert('Error: ' + error); + return false; }); } - showPopup(textSource, content) { - this.popup.showNextTo(textSource.getRect(), content); - - if (this.options.selectMatchedText) { - textSource.select(); - } - - this.lastTextSource = textSource; - } - - hidePopup() { + searchClear() { this.popup.hide(); if (this.options.selectMatchedText && this.lastTextSource !== null) { @@ -258,7 +206,6 @@ class Driver { } this.lastTextSource = null; - this.definitions = null; } api_setOptions(opts) { @@ -267,68 +214,9 @@ class Driver { api_setEnabled(enabled) { if (!(this.enabled = enabled)) { - this.hidePopup(); + this.searchClear(); } } - - api_addNote({index, mode}) { - const state = {[mode]: false}; - addDefinition(this.definitions[index], mode).then(success => { - if (success) { - this.popup.invokeApi('setActionState', {index, state, sequence: this.sequence}); - } else { - alert('Note could not be added'); - } - }).catch(error => { - alert('Error: ' + error); - }); - } - - api_playAudio(index) { - const definition = this.definitions[index]; - - let url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=${encodeURIComponent(definition.expression)}`; - if (definition.reading) { - url += `&kana=${encodeURIComponent(definition.reading)}`; - } - - for (const key in this.audio) { - this.audio[key].pause(); - } - - const audio = this.audio[url] || new Audio(url); - audio.currentTime = 0; - audio.play(); - - this.audio[url] = audio; - } - - api_displayKanji(kanji) { - findKanji(kanji).then(definitions => { - definitions.forEach(definition => definition.url = window.location.href); - - const sequence = ++this.sequence; - const context = { - definitions, - sequence, - addable: this.options.ankiMethod !== 'disabled', - root: this.fgRoot, - options: this.options - }; - - return renderText(context, 'kanji-list.html').then(content => { - this.definitions = definitions; - this.popup.setContent(content, definitions); - return canAddDefinitions(definitions, ['kanji']); - }).then(states => { - if (states !== null) { - states.forEach((state, index) => this.popup.invokeApi('setActionState', {index, state, sequence})); - } - }); - }).catch(error => { - alert('Error: ' + error); - }); - } } window.driver = new Driver(); diff --git a/ext/fg/js/frame.js b/ext/fg/js/frame.js index 8a99a405..4295dbb3 100644 --- a/ext/fg/js/frame.js +++ b/ext/fg/js/frame.js @@ -16,65 +16,145 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +class Frame { + constructor() { + this.definitions = []; + this.audioCache = {}; + this.sequence = 0; -function invokeApi(action, params, target) { - target.postMessage({action, params}, '*'); -} + $(window).on('message', e => { + const {action, params} = e.originalEvent.data, method = this['api_' + action]; + if (typeof(method) === 'function') { + method.call(this, params); + } + }); + } + + api_showTermDefs({definitions, options}) { + const sequence = ++this.sequence; + const context = { + definitions, + grouped: options.groupTermResults, + addable: options.ankiMethod !== 'disabled', + playback: options.enableAudioPlayback + }; + + this.definitions = definitions; + this.showSpinner(false); + window.scrollTo(0, 0); + + renderText(context, 'term-list.html').then(content => { + $('.content').html(content); + $('.action-add-note').click(this.onAddNote.bind(this)); + + $('.kanji-link').click(e => { + e.preventDefault(); + findKanji($(e.target).text()).then(kdefs => this.api_showKanjiDefs({options, definitions: kdefs})); + }); + + $('.action-play-audio').click(e => { + e.preventDefault(); + const index = $(e.currentTarget).data('index'); + this.playAudio(this.definitions[index]); + }); -function registerKanjiLinks() { - for (const link of Array.from(document.getElementsByClassName('kanji-link'))) { - link.addEventListener('click', e => { - e.preventDefault(); - invokeApi('displayKanji', e.target.innerHTML, window.parent); + this.updateAddNoteButtons(['term_kanji', 'term_kana'], sequence); }); } -} -function registerAddNoteLinks() { - for (const link of Array.from(document.getElementsByClassName('action-add-note'))) { - link.addEventListener('click', e => { - e.preventDefault(); - const ds = e.currentTarget.dataset; - invokeApi('addNote', {index: ds.index, mode: ds.mode}, window.parent); + api_showKanjiDefs({definitions, options}) { + const sequence = ++this.sequence; + const context = { + definitions, + addable: options.ankiMethod !== 'disabled' + }; + + this.definitions = definitions; + this.showSpinner(false); + window.scrollTo(0, 0); + + renderText(context, 'kanji-list.html').then(content => { + $('.content').html(content); + $('.action-add-note').click(this.onAddNote.bind(this)); + + this.updateAddNoteButtons(['kanji'], sequence); }); } -} -function registerAudioLinks() { - for (const link of Array.from(document.getElementsByClassName('action-play-audio'))) { - link.addEventListener('click', e => { - e.preventDefault(); - const ds = e.currentTarget.dataset; - invokeApi('playAudio', ds.index, window.parent); + findAddNoteButton(index, mode) { + return $(`.action-add-note[data-index="${index}"][data-mode="${mode}"]`); + } + + onAddNote(e) { + e.preventDefault(); + this.showSpinner(true); + + const link = $(e.currentTarget); + const index = link.data('index'); + const mode = link.data('mode'); + + addDefinition(this.definitions[index], mode).then(success => { + if (success) { + const button = this.findAddNoteButton(index, mode); + button.addClass('disabled'); + } else { + window.alert('Note could not be added'); + } + }).catch(error => { + window.alert('Error: ' + error); + }).then(() => { + this.showSpinner(false); }); } -} -function api_setActionState({index, state, sequence}) { - for (const mode in state) { - const matches = document.querySelectorAll(`.action-bar[data-sequence="${sequence}"] .action-add-note[data-index="${index}"][data-mode="${mode}"]`); - if (matches.length === 0) { - return; - } + updateAddNoteButtons(modes, sequence) { + canAddDefinitions(this.definitions, modes).then(states => { + if (states === null) { + return; + } - const classes = matches[0].classList; - if (state[mode]) { - classes.remove('disabled'); + if (sequence !== this.sequence) { + return; + } + + states.forEach((state, index) => { + for (const mode in state) { + const button = this.findAddNoteButton(index, mode); + if (state[mode]) { + button.removeClass('disabled'); + } else { + button.addClass('disabled'); + } + + button.removeClass('pending'); + } + }); + }); + } + + showSpinner(show) { + const spinner = $('.spinner'); + if (show) { + spinner.show(); } else { - classes.add('disabled'); + spinner.hide(); } } -} -document.addEventListener('DOMContentLoaded', () => { - registerKanjiLinks(); - registerAddNoteLinks(); - registerAudioLinks(); -}); + playAudio(definition) { + let url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=${encodeURIComponent(definition.expression)}`; + if (definition.reading) { + url += `&kana=${encodeURIComponent(definition.reading)}`; + } -window.addEventListener('message', e => { - const {action, params} = e.data, method = window['api_' + action]; - if (typeof(method) === 'function') { - method(params); + for (const key in this.audioCache) { + this.audioCache[key].pause(); + } + + const audio = this.audioCache[url] || new Audio(url); + audio.currentTime = 0; + audio.play(); } -}); +} + +window.frame = new Frame(); diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 8e71fefa..d47ab4ae 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -19,25 +19,26 @@ class Popup { constructor() { - this.container = null; this.offset = 10; - } - show(rect, content) { - this.inject(); + this.container = document.createElement('iframe'); + this.container.id = 'yomichan-popup'; + this.container.addEventListener('mousedown', e => e.stopPropagation()); + this.container.addEventListener('scroll', e => e.stopPropagation()); + this.container.setAttribute('src', chrome.extension.getURL('fg/frame.html')); + + document.body.appendChild(this.container); + } + showAt(rect) { this.container.style.left = rect.x + 'px'; this.container.style.top = rect.y + 'px'; this.container.style.height = rect.height + 'px'; this.container.style.width = rect.width + 'px'; this.container.style.visibility = 'visible'; - - this.setContent(content); } - showNextTo(elementRect, content) { - this.inject(); - + showNextTo(elementRect) { const containerStyle = window.getComputedStyle(this.container); const containerHeight = parseInt(containerStyle.height); const containerWidth = parseInt(containerStyle.width); @@ -56,48 +57,26 @@ class Popup { y = elementRect.top - height - this.offset; } - this.show({x, y, width, height}, content); - } - - visible() { - return this.container !== null && this.container.style.visibility !== 'hidden'; + this.showAt({x, y, width, height}); } hide() { - if (this.container !== null) { - this.container.style.visibility = 'hidden'; - } + this.container.style.visibility = 'hidden'; } - setContent(content) { - if (this.container === null) { - return; - } - - this.container.contentWindow.scrollTo(0, 0); - - const doc = this.container.contentDocument; - doc.open(); - doc.write(content); - doc.close(); + isVisible() { + return this.container.style.visibility !== 'hidden'; } - invokeApi(action, params) { - if (this.container !== null) { - this.container.contentWindow.postMessage({action, params}, '*'); - } + showTermDefs(definitions, options) { + this.invokeApi('showTermDefs', {definitions, options}); } - inject() { - if (this.container !== null) { - return; - } - - this.container = document.createElement('iframe'); - this.container.id = 'yomichan-popup'; - this.container.addEventListener('mousedown', e => e.stopPropagation()); - this.container.addEventListener('scroll', e => e.stopPropagation()); + showKanjiDefs(definitions, options) { + this.invokeApi('showKanjiDefs', {definitions, options}); + } - document.body.appendChild(this.container); + invokeApi(action, params) { + this.container.contentWindow.postMessage({action, params}, '*'); } } diff --git a/ext/fg/js/util.js b/ext/fg/js/util.js index 96007b74..cedfb887 100644 --- a/ext/fg/js/util.js +++ b/ext/fg/js/util.js @@ -17,7 +17,7 @@ */ -function invokeApiBg(action, params) { +function invokeBgApi(action, params) { return new Promise((resolve, reject) => { chrome.runtime.sendMessage({action, params}, ({result, error}) => { if (error) { @@ -29,32 +29,36 @@ function invokeApiBg(action, params) { }); } -function getEnabled() { - return invokeApiBg('getEnabled', {}); +function isEnabled() { + return invokeBgApi('getEnabled', {}); } function getOptions() { - return invokeApiBg('getOptions', {}); + return invokeBgApi('getOptions', {}); } function findTerm(text) { - return invokeApiBg('findTerm', {text}); + return invokeBgApi('findTerm', {text}); +} + +function findTermGrouped(text) { + return invokeBgApi('findTermGrouped', {text}); } function findKanji(text) { - return invokeApiBg('findKanji', {text}); + return invokeBgApi('findKanji', {text}); } function renderText(data, template) { - return invokeApiBg('renderText', {data, template}); + return invokeBgApi('renderText', {data, template}); } function canAddDefinitions(definitions, modes) { - return invokeApiBg('canAddDefinitions', {definitions, modes}).catch(() => null); + return invokeBgApi('canAddDefinitions', {definitions, modes}).catch(() => null); } function addDefinition(definition, mode) { - return invokeApiBg('addDefinition', {definition, mode}); + return invokeBgApi('addDefinition', {definition, mode}); } function textSourceFromPoint(point) { |