/* * Copyright (C) 2016-2020 Yomichan Authors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /* global * ClipboardMonitor * Display * DocumentUtil * api * wanakana */ class DisplaySearch extends Display { constructor() { super(document.querySelector('#spinner'), document.querySelector('#content')); this._searchButton = document.querySelector('#search'); this._queryInput = document.querySelector('#query'); this._introElement = document.querySelector('#intro'); this._clipboardMonitorEnableCheckbox = document.querySelector('#clipboard-monitor-enable'); this._wanakanaEnableCheckbox = document.querySelector('#wanakana-enable'); this._isPrepared = false; this._introVisible = true; this._introAnimationTimer = null; this._clipboardMonitorEnabled = false; this._clipboardMonitor = new ClipboardMonitor({ getClipboard: api.clipboardGet.bind(api) }); this._onKeyDownIgnoreKeys = new Map([ ['ANY_MOD', new Set([ 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20', 'F21', 'F22', 'F23', 'F24' ])], ['Control', new Set(['C', 'A', 'Z', 'Y', 'X', 'F', 'G'])], ['Meta', new Set(['C', 'A', 'Z', 'Y', 'X', 'F', 'G'])], ['OS', new Set()], ['Alt', new Set()], ['AltGraph', new Set()], ['Shift', new Set()] ]); } async prepare() { await super.prepare(); await this.updateOptions(); yomichan.on('optionsUpdated', () => this.updateOptions()); this.on('contentUpdating', this._onContentUpdating.bind(this)); this.on('modeChange', this._onModeChange.bind(this)); this.registerMessageHandlers([ ['updateSearchQuery', {async: false, handler: this._onExternalSearchUpdate.bind(this)}] ]); this.queryParserVisible = true; this.setHistorySettings({useBrowserHistory: true}); const options = this.getOptions(); if (options.general.enableWanakana === true) { this._wanakanaEnableCheckbox.checked = true; wanakana.bind(this._queryInput); } else { this._wanakanaEnableCheckbox.checked = false; } this._searchButton.addEventListener('click', this._onSearch.bind(this), false); this._queryInput.addEventListener('input', this._onSearchInput.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._updateSearchButton(); this._onModeChange(); await this._prepareNestedPopups(); this.initializeState(); this._isPrepared = true; } onEscape() { if (this._queryInput === null) { return; } this._queryInput.focus(); this._queryInput.select(); } onKeyDown(e) { const key = DocumentUtil.getKeyFromEvent(e); const ignoreKeys = this._onKeyDownIgnoreKeys; const activeModifierMap = new Map([ ['Control', e.ctrlKey], ['Meta', e.metaKey], ['Shift', e.shiftKey], ['Alt', e.altKey], ['ANY_MOD', true] ]); let preventFocus = false; for (const [modifier, keys] of ignoreKeys.entries()) { const modifierActive = activeModifierMap.get(modifier); if (key === modifier || (modifierActive && keys.has(key))) { preventFocus = true; break; } } if (!super.onKeyDown(e) && !preventFocus && document.activeElement !== this._queryInput) { this._queryInput.focus({preventScroll: true}); } } async updateOptions() { await super.updateOptions(); if (!this._isPrepared) { return; } const query = this._queryInput.value; if (query) { this._onSearchQueryUpdated(query, false); } } postProcessQuery(query) { if (this._isWanakanaEnabled()) { try { query = wanakana.toKana(query); } catch (e) { // NOP } } return query; } // Private _onContentUpdating({type, content, source}) { let animate = false; let valid = false; switch (type) { case 'terms': case 'kanji': animate = content.animate; valid = content.definitions.length > 0; this._queryInput.blur(); break; case 'clear': valid = false; animate = true; source = ''; break; } if (typeof source !== 'string') { source = ''; } this._queryInput.value = source; this._setIntroVisible(!valid, animate); this._updateSearchButton(); } _onSearchInput() { this._updateSearchButton(); const queryElementRect = this._queryInput.getBoundingClientRect(); if (queryElementRect.top < 0 || queryElementRect.bottom > window.innerHeight) { this._queryInput.scrollIntoView(); } } _onSearch(e) { if (this._queryInput === null) { return; } e.preventDefault(); const query = this._queryInput.value; this._onSearchQueryUpdated(query, true); } _onCopy() { // ignore copy from search page this._clipboardMonitor.setPreviousText(window.getSelection().toString().trim()); } _onExternalSearchUpdate({text, animate=true}) { this._onSearchQueryUpdated(text, animate); } _onSearchQueryUpdated(query, animate) { const details = { focus: false, history: false, params: { query }, state: { focusEntry: 0, sentence: {text: query, offset: 0}, url: window.location.href }, content: { definitions: null, animate } }; this.setContent(details); } _onWanakanaEnableChange(e) { const value = e.target.checked; if (value) { wanakana.bind(this._queryInput); } else { wanakana.unbind(this._queryInput); } api.modifySettings([{ action: 'set', path: 'general.enableWanakana', value, scope: 'profile', optionsContext: this.getOptionsContext() }], 'search'); } _onClipboardMonitorEnableChange(e) { const enabled = e.target.checked; this._setClipboardMonitorEnabled(enabled); } _onModeChange() { let mode = this.mode; if (mode === null) { mode = ''; } document.documentElement.dataset.searchMode = mode; this._updateClipboardMonitorEnabled(); } _isWanakanaEnabled() { return this._wanakanaEnableCheckbox !== null && this._wanakanaEnableCheckbox.checked; } _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'; } _updateSearchButton() { this._searchButton.disabled = this._introVisible && (this._queryInput === null || this._queryInput.value.length === 0); } async _prepareNestedPopups() { let complete = false; const onOptionsUpdated = async () => { const optionsContext = this.getOptionsContext(); const options = await api.optionsGet(optionsContext); if (!options.scanning.enableOnSearchPage || complete) { return; } complete = true; yomichan.off('optionsUpdated', onOptionsUpdated); try { await this.setupNestedPopups({ depth: 1, proxy: false, isSearchPage: true }); } catch (e) { yomichan.logError(e); } }; yomichan.on('optionsUpdated', onOptionsUpdated); await onOptionsUpdated(); } 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: 'general.enableClipboardMonitor', value, scope: 'profile', optionsContext: this.getOptionsContext() }], 'search'); } _updateClipboardMonitorEnabled() { const mode = this.mode; const enabled = this._clipboardMonitorEnabled; this._clipboardMonitorEnableCheckbox.checked = enabled; if (enabled && 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); } ); }); } }