diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-02-14 11:19:54 -0500 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-02-14 11:19:54 -0500 | 
| commit | e419a418f6f03ef0a24330b67e7b76c5e3a7c22d (patch) | |
| tree | a4c27bdfabc9280d9f6262d93d5152a58de8bc15 /ext/js/display | |
| parent | 43d1457ebfe23196348649c245dfb942a0f00a1a (diff) | |
Move bg/js (#1387)
* Move bg/js/anki.js to js/comm/anki.js
* Move bg/js/mecab.js to js/comm/mecab.js
* Move bg/js/search-main.js to js/display/search-main.js
* Move bg/js/template-patcher.js to js/templates/template-patcher.js
* Move bg/js/template-renderer-frame-api.js to js/templates/template-renderer-frame-api.js
* Move bg/js/template-renderer-frame-main.js to js/templates/template-renderer-frame-main.js
* Move bg/js/template-renderer-proxy.js to js/templates/template-renderer-proxy.js
* Move bg/js/template-renderer.js to js/templates/template-renderer.js
* Move bg/js/media-utility.js to js/media/media-utility.js
* Move bg/js/native-simple-dom-parser.js to js/dom/native-simple-dom-parser.js
* Move bg/js/simple-dom-parser.js to js/dom/simple-dom-parser.js
* Move bg/js/audio-downloader.js to js/media/audio-downloader.js
* Move bg/js/deinflector.js to js/language/deinflector.js
* Move bg/js/backend.js to js/background/backend.js
* Move bg/js/translator.js to js/language/translator.js
* Move bg/js/search-display-controller.js to js/display/search-display-controller.js
* Move bg/js/request-builder.js to js/background/request-builder.js
* Move bg/js/text-source-map.js to js/general/text-source-map.js
* Move bg/js/clipboard-reader.js to js/comm/clipboard-reader.js
* Move bg/js/clipboard-monitor.js to js/comm/clipboard-monitor.js
* Move bg/js/query-parser.js to js/display/query-parser.js
* Move bg/js/profile-conditions.js to js/background/profile-conditions.js
* Move bg/js/dictionary-database.js to js/language/dictionary-database.js
* Move bg/js/dictionary-importer.js to js/language/dictionary-importer.js
* Move bg/js/anki-note-builder.js to js/data/anki-note-builder.js
* Move bg/js/anki-note-data.js to js/data/anki-note-data.js
* Move bg/js/database.js to js/data/database.js
* Move bg/js/json-schema.js to js/data/json-schema.js
* Move bg/js/options.js to js/data/options-util.js
* Move bg/js/background-main.js to js/background/background-main.js
* Move bg/js/permissions-util.js to js/data/permissions-util.js
* Move bg/js/context-main.js to js/pages/action-popup-main.js
* Move bg/js/generic-page-main.js to js/pages/generic-page-main.js
* Move bg/js/info-main.js to js/pages/info-main.js
* Move bg/js/permissions-main.js to js/pages/permissions-main.js
* Move bg/js/welcome-main.js to js/pages/welcome-main.js
Diffstat (limited to 'ext/js/display')
| -rw-r--r-- | ext/js/display/query-parser.js | 232 | ||||
| -rw-r--r-- | ext/js/display/search-display-controller.js | 422 | ||||
| -rw-r--r-- | ext/js/display/search-main.js | 57 | 
3 files changed, 711 insertions, 0 deletions
diff --git a/ext/js/display/query-parser.js b/ext/js/display/query-parser.js new file mode 100644 index 00000000..05ebfa27 --- /dev/null +++ b/ext/js/display/query-parser.js @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2019-2021  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 <https://www.gnu.org/licenses/>. + */ + +/* global + * TextScanner + * api + */ + +class QueryParser extends EventDispatcher { +    constructor({getSearchContext, documentUtil}) { +        super(); +        this._getSearchContext = getSearchContext; +        this._documentUtil = documentUtil; +        this._text = ''; +        this._setTextToken = null; +        this._selectedParser = null; +        this._parseResults = []; +        this._queryParser = document.querySelector('#query-parser-content'); +        this._queryParserModeContainer = document.querySelector('#query-parser-mode-container'); +        this._queryParserModeSelect = document.querySelector('#query-parser-mode-select'); +        this._textScanner = new TextScanner({ +            node: this._queryParser, +            getSearchContext, +            documentUtil, +            searchTerms: true, +            searchKanji: false, +            searchOnClick: true +        }); +    } + +    get text() { +        return this._text; +    } + +    prepare() { +        this._textScanner.prepare(); +        this._textScanner.on('searched', this._onSearched.bind(this)); +        this._queryParserModeSelect.addEventListener('change', this._onParserChange.bind(this), false); +    } + +    setOptions({selectedParser, termSpacing, scanning}) { +        let selectedParserChanged = false; +        if (selectedParser === null || typeof selectedParser === 'string') { +            selectedParserChanged = (this._selectedParser !== selectedParser); +            this._selectedParser = selectedParser; +        } +        if (typeof termSpacing === 'boolean') { +            this._queryParser.dataset.termSpacing = `${termSpacing}`; +        } +        if (scanning !== null && typeof scanning === 'object') { +            this._textScanner.setOptions(scanning); +        } +        this._textScanner.setEnabled(true); +        if (selectedParserChanged && this._parseResults.length > 0) { +            this._renderParseResult(); +        } +    } + +    async setText(text) { +        this._text = text; +        this._setPreview(text); + +        const token = {}; +        this._setTextToken = token; +        this._parseResults = await api.textParse(text, this._getOptionsContext()); +        if (this._setTextToken !== token) { return; } + +        this._refreshSelectedParser(); + +        this._renderParserSelect(); +        this._renderParseResult(); +    } + +    // Private + +    _onSearched(e) { +        const {error} = e; +        if (error !== null) { +            yomichan.logError(error); +            return; +        } +        if (e.type === null) { return; } + +        this.trigger('searched', e); +    } + +    _onParserChange(e) { +        const value = e.currentTarget.value; +        this._setSelectedParser(value); +    } + +    _getOptionsContext() { +        return this._getSearchContext().optionsContext; +    } + +    _refreshSelectedParser() { +        if (this._parseResults.length > 0 && !this._getParseResult()) { +            const value = this._parseResults[0].id; +            this._setSelectedParser(value); +        } +    } + +    _setSelectedParser(value) { +        const optionsContext = this._getOptionsContext(); +        api.modifySettings([{ +            action: 'set', +            path: 'parsing.selectedParser', +            value, +            scope: 'profile', +            optionsContext +        }], 'search'); +    } + +    _getParseResult() { +        const selectedParser = this._selectedParser; +        return this._parseResults.find((r) => r.id === selectedParser); +    } + +    _setPreview(text) { +        const terms = [[{text, reading: ''}]]; +        this._queryParser.textContent = ''; +        this._queryParser.appendChild(this._createParseResult(terms, true)); +    } + +    _renderParserSelect() { +        const visible = (this._parseResults.length > 1); +        if (visible) { +            this._updateParserModeSelect(this._queryParserModeSelect, this._parseResults, this._selectedParser); +        } +        this._queryParserModeContainer.hidden = !visible; +    } + +    _renderParseResult() { +        const parseResult = this._getParseResult(); +        this._queryParser.textContent = ''; +        if (!parseResult) { return; } +        this._queryParser.appendChild(this._createParseResult(parseResult.content, false)); +    } + +    _updateParserModeSelect(select, parseResults, selectedParser) { +        const fragment = document.createDocumentFragment(); + +        let index = 0; +        let selectedIndex = -1; +        for (const parseResult of parseResults) { +            const option = document.createElement('option'); +            option.value = parseResult.id; +            switch (parseResult.source) { +                case 'scanning-parser': +                    option.textContent = 'Scanning parser'; +                    break; +                case 'mecab': +                    option.textContent = `MeCab: ${parseResult.dictionary}`; +                    break; +                default: +                    option.textContent = `Unknown source: ${parseResult.source}`; +                    break; +            } +            fragment.appendChild(option); + +            if (selectedParser === parseResult.id) { +                selectedIndex = index; +            } +            ++index; +        } + +        select.textContent = ''; +        select.appendChild(fragment); +        select.selectedIndex = selectedIndex; +    } + +    _createParseResult(terms, preview) { +        const type = preview ? 'preview' : 'normal'; +        const fragment = document.createDocumentFragment(); +        for (const term of terms) { +            const termNode = document.createElement('span'); +            termNode.className = 'query-parser-term'; +            termNode.dataset.type = type; +            for (const segment of term) { +                if (segment.reading.trim().length === 0) { +                    this._addSegmentText(segment.text, termNode); +                } else { +                    termNode.appendChild(this._createSegment(segment)); +                } +            } +            fragment.appendChild(termNode); +        } +        return fragment; +    } + +    _createSegment(segment) { +        const segmentNode = document.createElement('ruby'); +        segmentNode.className = 'query-parser-segment'; + +        const textNode = document.createElement('span'); +        textNode.className = 'query-parser-segment-text'; + +        const readingNode = document.createElement('rt'); +        readingNode.className = 'query-parser-segment-reading'; + +        segmentNode.appendChild(textNode); +        segmentNode.appendChild(readingNode); + +        this._addSegmentText(segment.text, textNode); +        readingNode.textContent = segment.reading; + +        return segmentNode; +    } + +    _addSegmentText(text, container) { +        for (const character of text) { +            const node = document.createElement('span'); +            node.className = 'query-parser-char'; +            node.textContent = character; +            container.appendChild(node); +        } +    } +} diff --git a/ext/js/display/search-display-controller.js b/ext/js/display/search-display-controller.js new file mode 100644 index 00000000..a295346d --- /dev/null +++ b/ext/js/display/search-display-controller.js @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2016-2021  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 <https://www.gnu.org/licenses/>. + */ + +/* global + * ClipboardMonitor + * api + * wanakana + */ + +class SearchDisplayController { +    constructor(tabId, frameId, display, japaneseUtil) { +        this._tabId = tabId; +        this._frameId = frameId; +        this._display = display; +        this._searchButton = document.querySelector('#search-button'); +        this._queryInput = document.querySelector('#search-textbox'); +        this._introElement = document.querySelector('#intro'); +        this._clipboardMonitorEnableCheckbox = document.querySelector('#clipboard-monitor-enable'); +        this._wanakanaEnableCheckbox = document.querySelector('#wanakana-enable'); +        this._queryInputEvents = new EventListenerCollection(); +        this._queryInputEventsSetup = false; +        this._wanakanaEnabled = false; +        this._introVisible = true; +        this._introAnimationTimer = null; +        this._clipboardMonitorEnabled = false; +        this._clipboardMonitor = new ClipboardMonitor({ +            japaneseUtil, +            clipboardReader: { +                getText: async () => (await api.clipboardGet()) +            } +        }); +        this._messageHandlers = new Map(); +        this._mode = null; +    } + +    async prepare() { +        this._updateMode(); + +        await this._display.updateOptions(); + +        chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); +        yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this)); + +        this._display.on('optionsUpdated', this._onDisplayOptionsUpdated.bind(this)); +        this._display.on('contentUpdating', this._onContentUpdating.bind(this)); + +        this._display.hotkeyHandler.registerActions([ +            ['focusSearchBox', this._onActionFocusSearchBox.bind(this)] +        ]); +        this._registerMessageHandlers([ +            ['getMode', {async: false, handler: this._onMessageGetMode.bind(this)}], +            ['setMode', {async: false, handler: this._onMessageSetMode.bind(this)}], +            ['updateSearchQuery', {async: false, handler: this._onExternalSearchUpdate.bind(this)}] +        ]); + +        this._display.autoPlayAudioDelay = 0; +        this._display.queryParserVisible = true; +        this._display.setHistorySettings({useBrowserHistory: true}); +        this._display.setQueryPostProcessor(this._postProcessQuery.bind(this)); + +        this._searchButton.addEventListener('click', this._onSearch.bind(this), false); +        this._wanakanaEnableCheckbox.addEventListener('change', this._onWanakanaEnableChange.bind(this)); +        window.addEventListener('copy', this._onCopy.bind(this)); +        this._clipboardMonitor.on('change', this._onExternalSearchUpdate.bind(this)); +        this._clipboardMonitorEnableCheckbox.addEventListener('change', this._onClipboardMonitorEnableChange.bind(this)); +        this._display.hotkeyHandler.on('keydownNonHotkey', this._onKeyDown.bind(this)); + +        this._onDisplayOptionsUpdated({options: this._display.getOptions()}); +    } + +    // Actions + +    _onActionFocusSearchBox() { +        if (this._queryInput === null) { return; } +        this._queryInput.focus(); +        this._queryInput.select(); +    } + +    // Messages + +    _onMessageSetMode({mode}) { +        this._setMode(mode, true); +    } + +    _onMessageGetMode() { +        return this._mode; +    } + +    // Private + +    _onMessage({action, params}, sender, callback) { +        const messageHandler = this._messageHandlers.get(action); +        if (typeof messageHandler === 'undefined') { return false; } +        return yomichan.invokeMessageHandler(messageHandler, params, callback, sender); +    } + +    _onKeyDown(e) { +        if ( +            document.activeElement !== this._queryInput && +            !e.ctrlKey && +            !e.metaKey && +            !e.altKey && +            e.key.length === 1 +        ) { +            this._queryInput.focus({preventScroll: true}); +        } +    } + +    async _onOptionsUpdated() { +        await this._display.updateOptions(); +        const query = this._queryInput.value; +        if (query) { +            this._display.searchLast(); +        } +    } + +    _onDisplayOptionsUpdated({options}) { +        this._clipboardMonitorEnabled = options.clipboard.enableSearchPageMonitor; +        this._updateClipboardMonitorEnabled(); + +        const enableWanakana = !!this._display.getOptions().general.enableWanakana; +        this._wanakanaEnableCheckbox.checked = enableWanakana; +        this._setWanakanaEnabled(enableWanakana); +    } + +    _onContentUpdating({type, content, source}) { +        let animate = false; +        let valid = false; +        switch (type) { +            case 'terms': +            case 'kanji': +                animate = !!content.animate; +                valid = (typeof source === 'string' && source.length > 0); +                this._display.blurElement(this._queryInput); +                break; +            case 'clear': +                valid = false; +                animate = true; +                source = ''; +                break; +        } + +        if (typeof source !== 'string') { source = ''; } + +        if (this._queryInput.value !== source) { +            this._queryInput.value = source; +            this._updateSearchHeight(true); +        } +        this._setIntroVisible(!valid, animate); +    } + +    _onSearchInput() { +        this._updateSearchHeight(false); +    } + +    _onSearchKeydown(e) { +        const {code} = e; +        if (!((code === 'Enter' || code === 'NumpadEnter') && !e.shiftKey)) { return; } + +        // Search +        e.preventDefault(); +        e.stopImmediatePropagation(); +        this._display.blurElement(e.currentTarget); +        this._search(true, true, true); +    } + +    _onSearch(e) { +        e.preventDefault(); +        this._search(true, true, true); +    } + +    _onCopy() { +        // ignore copy from search page +        this._clipboardMonitor.setPreviousText(window.getSelection().toString().trim()); +    } + +    _onExternalSearchUpdate({text, animate=true}) { +        const {clipboard: {autoSearchContent, maximumSearchLength}} = this._display.getOptions(); +        if (text.length > maximumSearchLength) { +            text = text.substring(0, maximumSearchLength); +        } +        this._queryInput.value = text; +        this._updateSearchHeight(true); +        this._search(animate, false, autoSearchContent); +    } + +    _onWanakanaEnableChange(e) { +        const value = e.target.checked; +        this._setWanakanaEnabled(value); +        api.modifySettings([{ +            action: 'set', +            path: 'general.enableWanakana', +            value, +            scope: 'profile', +            optionsContext: this._display.getOptionsContext() +        }], 'search'); +    } + +    _onClipboardMonitorEnableChange(e) { +        const enabled = e.target.checked; +        this._setClipboardMonitorEnabled(enabled); +    } + +    _setWanakanaEnabled(enabled) { +        if (this._queryInputEventsSetup && this._wanakanaEnabled === enabled) { return; } + +        const input = this._queryInput; +        this._queryInputEvents.removeAllEventListeners(); +        this._queryInputEvents.addEventListener(input, 'keydown', this._onSearchKeydown.bind(this), false); + +        this._wanakanaEnabled = enabled; +        if (enabled) { +            wanakana.bind(input); +        } else { +            wanakana.unbind(input); +        } + +        this._queryInputEvents.addEventListener(input, 'input', this._onSearchInput.bind(this), false); +        this._queryInputEventsSetup = true; +    } + +    _setIntroVisible(visible, animate) { +        if (this._introVisible === visible) { +            return; +        } + +        this._introVisible = visible; + +        if (this._introElement === null) { +            return; +        } + +        if (this._introAnimationTimer !== null) { +            clearTimeout(this._introAnimationTimer); +            this._introAnimationTimer = null; +        } + +        if (visible) { +            this._showIntro(animate); +        } else { +            this._hideIntro(animate); +        } +    } + +    _showIntro(animate) { +        if (animate) { +            const duration = 0.4; +            this._introElement.style.transition = ''; +            this._introElement.style.height = ''; +            const size = this._introElement.getBoundingClientRect(); +            this._introElement.style.height = '0px'; +            this._introElement.style.transition = `height ${duration}s ease-in-out 0s`; +            window.getComputedStyle(this._introElement).getPropertyValue('height'); // Commits height so next line can start animation +            this._introElement.style.height = `${size.height}px`; +            this._introAnimationTimer = setTimeout(() => { +                this._introElement.style.height = ''; +                this._introAnimationTimer = null; +            }, duration * 1000); +        } else { +            this._introElement.style.transition = ''; +            this._introElement.style.height = ''; +        } +    } + +    _hideIntro(animate) { +        if (animate) { +            const duration = 0.4; +            const size = this._introElement.getBoundingClientRect(); +            this._introElement.style.height = `${size.height}px`; +            this._introElement.style.transition = `height ${duration}s ease-in-out 0s`; +            window.getComputedStyle(this._introElement).getPropertyValue('height'); // Commits height so next line can start animation +        } else { +            this._introElement.style.transition = ''; +        } +        this._introElement.style.height = '0'; +    } + +    async _setClipboardMonitorEnabled(value) { +        let modify = true; +        if (value) { +            value = await this._requestPermissions(['clipboardRead']); +            modify = value; +        } + +        this._clipboardMonitorEnabled = value; +        this._updateClipboardMonitorEnabled(); + +        if (!modify) { return; } + +        await api.modifySettings([{ +            action: 'set', +            path: 'clipboard.enableSearchPageMonitor', +            value, +            scope: 'profile', +            optionsContext: this._display.getOptionsContext() +        }], 'search'); +    } + +    _updateClipboardMonitorEnabled() { +        const enabled = this._clipboardMonitorEnabled; +        this._clipboardMonitorEnableCheckbox.checked = enabled; +        if (enabled && this._mode !== 'popup') { +            this._clipboardMonitor.start(); +        } else { +            this._clipboardMonitor.stop(); +        } +    } + +    _requestPermissions(permissions) { +        return new Promise((resolve) => { +            chrome.permissions.request( +                {permissions}, +                (granted) => { +                    const e = chrome.runtime.lastError; +                    resolve(!e && granted); +                } +            ); +        }); +    } + +    _search(animate, history, lookup) { +        const query = this._queryInput.value; +        const depth = this._display.depth; +        const url = window.location.href; +        const documentTitle = document.title; +        const details = { +            focus: false, +            history, +            params: { +                query +            }, +            state: { +                focusEntry: 0, +                optionsContext: {depth, url}, +                url, +                sentence: {text: query, offset: 0}, +                documentTitle +            }, +            content: { +                definitions: null, +                animate, +                contentOrigin: { +                    tabId: this.tabId, +                    frameId: this.frameId +                } +            } +        }; +        if (!lookup) { details.params.lookup = 'false'; } +        this._display.setContent(details); +    } + +    _updateSearchHeight(shrink) { +        const node = this._queryInput; +        if (shrink) { +            node.style.height = '0'; +        } +        const {scrollHeight} = node; +        const currentHeight = node.getBoundingClientRect().height; +        if (shrink || scrollHeight >= currentHeight - 1) { +            node.style.height = `${scrollHeight}px`; +        } +    } + +    _postProcessQuery(query) { +        if (this._wanakanaEnabled) { +            try { +                query = this._japaneseUtil.convertToKana(query); +            } catch (e) { +                // NOP +            } +        } +        return query; +    } + +    _registerMessageHandlers(handlers) { +        for (const [name, handlerInfo] of handlers) { +            this._messageHandlers.set(name, handlerInfo); +        } +    } + +    _updateMode() { +        let mode = null; +        try { +            mode = sessionStorage.getItem('mode'); +        } catch (e) { +            // Browsers can throw a SecurityError when cookie blocking is enabled. +        } +        this._setMode(mode, false); +    } + +    _setMode(mode, save) { +        if (mode === this._mode) { return; } +        if (save) { +            try { +                if (mode === null) { +                    sessionStorage.removeItem('mode'); +                } else { +                    sessionStorage.setItem('mode', mode); +                } +            } catch (e) { +                // Browsers can throw a SecurityError when cookie blocking is enabled. +            } +        } +        this._mode = mode; +        document.documentElement.dataset.searchMode = (mode !== null ? mode : ''); +        this._updateClipboardMonitorEnabled(); +    } +} diff --git a/ext/js/display/search-main.js b/ext/js/display/search-main.js new file mode 100644 index 00000000..c7ec595a --- /dev/null +++ b/ext/js/display/search-main.js @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2019-2021  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 <https://www.gnu.org/licenses/>. + */ + +/* global + * Display + * DocumentFocusController + * HotkeyHandler + * JapaneseUtil + * SearchDisplayController + * api + * wanakana + */ + +(async () => { +    try { +        const documentFocusController = new DocumentFocusController(); +        documentFocusController.prepare(); + +        api.forwardLogsToBackend(); +        await yomichan.backendReady(); + +        const {tabId, frameId} = await api.frameInformationGet(); + +        const japaneseUtil = new JapaneseUtil(wanakana); + +        const hotkeyHandler = new HotkeyHandler(); +        hotkeyHandler.prepare(); + +        const display = new Display(tabId, frameId, 'search', japaneseUtil, documentFocusController, hotkeyHandler); +        await display.prepare(); + +        const searchDisplayController = new SearchDisplayController(tabId, frameId, display, japaneseUtil); +        await searchDisplayController.prepare(); + +        display.initializeState(); + +        document.documentElement.dataset.loaded = 'true'; + +        yomichan.ready(); +    } catch (e) { +        yomichan.logError(e); +    } +})();  |