summaryrefslogtreecommitdiff
path: root/ext/js/display/search-display-controller.js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-02-14 11:19:54 -0500
committerGitHub <noreply@github.com>2021-02-14 11:19:54 -0500
commite419a418f6f03ef0a24330b67e7b76c5e3a7c22d (patch)
treea4c27bdfabc9280d9f6262d93d5152a58de8bc15 /ext/js/display/search-display-controller.js
parent43d1457ebfe23196348649c245dfb942a0f00a1a (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/search-display-controller.js')
-rw-r--r--ext/js/display/search-display-controller.js422
1 files changed, 422 insertions, 0 deletions
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();
+ }
+}