Merge pull request #333 from siikamiika/native-popup-windows
Native popup windows
This commit is contained in:
commit
cbfae2b9d7
@ -26,6 +26,7 @@
|
|||||||
<script src="/bg/js/mecab.js"></script>
|
<script src="/bg/js/mecab.js"></script>
|
||||||
<script src="/bg/js/audio.js"></script>
|
<script src="/bg/js/audio.js"></script>
|
||||||
<script src="/bg/js/backend-api-forwarder.js"></script>
|
<script src="/bg/js/backend-api-forwarder.js"></script>
|
||||||
|
<script src="/bg/js/clipboard-monitor.js"></script>
|
||||||
<script src="/bg/js/conditions.js"></script>
|
<script src="/bg/js/conditions.js"></script>
|
||||||
<script src="/bg/js/database.js"></script>
|
<script src="/bg/js/database.js"></script>
|
||||||
<script src="/bg/js/deinflector.js"></script>
|
<script src="/bg/js/deinflector.js"></script>
|
||||||
|
@ -222,6 +222,20 @@ html:root[data-operating-system=openbsd] [data-show-for-operating-system~=openbs
|
|||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html:root[data-browser=edge] [data-hide-for-browser~=edge],
|
||||||
|
html:root[data-browser=chrome] [data-hide-for-browser~=chrome],
|
||||||
|
html:root[data-browser=firefox] [data-hide-for-browser~=firefox],
|
||||||
|
html:root[data-browser=firefox-mobile] [data-hide-for-browser~=firefox-mobile],
|
||||||
|
html:root[data-operating-system=mac] [data-hide-for-operating-system~=mac],
|
||||||
|
html:root[data-operating-system=win] [data-hide-for-operating-system~=win],
|
||||||
|
html:root[data-operating-system=android] [data-hide-for-operating-system~=android],
|
||||||
|
html:root[data-operating-system=cros] [data-hide-for-operating-system~=cros],
|
||||||
|
html:root[data-operating-system=linux] [data-hide-for-operating-system~=linux],
|
||||||
|
html:root[data-operating-system=openbsd] [data-hide-for-operating-system~=openbsd] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 740px) {
|
@media screen and (max-width: 740px) {
|
||||||
.col-xs-6 {
|
.col-xs-6 {
|
||||||
float: none;
|
float: none;
|
||||||
|
@ -79,6 +79,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"enable",
|
"enable",
|
||||||
|
"enableClipboardPopups",
|
||||||
"resultOutputMode",
|
"resultOutputMode",
|
||||||
"debugInfo",
|
"debugInfo",
|
||||||
"maxResults",
|
"maxResults",
|
||||||
@ -111,6 +112,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
|
"enableClipboardPopups": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"resultOutputMode": {
|
"resultOutputMode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["group", "merge", "split"],
|
"enum": ["group", "merge", "split"],
|
||||||
|
@ -29,6 +29,10 @@ function apiGetDisplayTemplatesHtml() {
|
|||||||
return _apiInvoke('getDisplayTemplatesHtml');
|
return _apiInvoke('getDisplayTemplatesHtml');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function apiClipboardGet() {
|
||||||
|
return _apiInvoke('clipboardGet');
|
||||||
|
}
|
||||||
|
|
||||||
function _apiInvoke(action, params={}) {
|
function _apiInvoke(action, params={}) {
|
||||||
const data = {action, params};
|
const data = {action, params};
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -22,6 +22,7 @@ class Backend {
|
|||||||
this.translator = new Translator();
|
this.translator = new Translator();
|
||||||
this.anki = new AnkiNull();
|
this.anki = new AnkiNull();
|
||||||
this.mecab = new Mecab();
|
this.mecab = new Mecab();
|
||||||
|
this.clipboardMonitor = new ClipboardMonitor();
|
||||||
this.options = null;
|
this.options = null;
|
||||||
this.optionsSchema = null;
|
this.optionsSchema = null;
|
||||||
this.optionsContext = {
|
this.optionsContext = {
|
||||||
@ -34,6 +35,8 @@ class Backend {
|
|||||||
|
|
||||||
this.clipboardPasteTarget = document.querySelector('#clipboard-paste-target');
|
this.clipboardPasteTarget = document.querySelector('#clipboard-paste-target');
|
||||||
|
|
||||||
|
this.popupWindow = null;
|
||||||
|
|
||||||
this.apiForwarder = new BackendApiForwarder();
|
this.apiForwarder = new BackendApiForwarder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +70,8 @@ class Backend {
|
|||||||
this.isPreparedResolve();
|
this.isPreparedResolve();
|
||||||
this.isPreparedResolve = null;
|
this.isPreparedResolve = null;
|
||||||
this.isPreparedPromise = null;
|
this.isPreparedPromise = null;
|
||||||
|
|
||||||
|
this.clipboardMonitor.onClipboardText = (text) => this._onClipboardText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
onOptionsUpdated(source) {
|
onOptionsUpdated(source) {
|
||||||
@ -97,6 +102,10 @@ class Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onClipboardText(text) {
|
||||||
|
this._onCommandSearch({mode: 'popup', query: text});
|
||||||
|
}
|
||||||
|
|
||||||
_onZoomChange({tabId, oldZoomFactor, newZoomFactor}) {
|
_onZoomChange({tabId, oldZoomFactor, newZoomFactor}) {
|
||||||
const callback = () => this.checkLastError(chrome.runtime.lastError);
|
const callback = () => this.checkLastError(chrome.runtime.lastError);
|
||||||
chrome.tabs.sendMessage(tabId, {action: 'zoomChanged', params: {oldZoomFactor, newZoomFactor}}, callback);
|
chrome.tabs.sendMessage(tabId, {action: 'zoomChanged', params: {oldZoomFactor, newZoomFactor}}, callback);
|
||||||
@ -121,6 +130,12 @@ class Backend {
|
|||||||
} else {
|
} else {
|
||||||
this.mecab.stopListener();
|
this.mecab.stopListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.general.enableClipboardPopups) {
|
||||||
|
this.clipboardMonitor.start();
|
||||||
|
} else {
|
||||||
|
this.clipboardMonitor.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOptionsSchema() {
|
async getOptionsSchema() {
|
||||||
@ -521,13 +536,30 @@ class Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _onApiClipboardGet() {
|
async _onApiClipboardGet() {
|
||||||
const clipboardPasteTarget = this.clipboardPasteTarget;
|
/*
|
||||||
clipboardPasteTarget.value = '';
|
Notes:
|
||||||
clipboardPasteTarget.focus();
|
document.execCommand('paste') doesn't work on Firefox.
|
||||||
document.execCommand('paste');
|
This may be a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
|
||||||
const result = clipboardPasteTarget.value;
|
Therefore, navigator.clipboard.readText() is used on Firefox.
|
||||||
clipboardPasteTarget.value = '';
|
|
||||||
return result;
|
navigator.clipboard.readText() can't be used in Chrome for two reasons:
|
||||||
|
* Requires page to be focused, else it rejects with an exception.
|
||||||
|
* When the page is focused, Chrome will request clipboard permission, despite already
|
||||||
|
being an extension with clipboard permissions. It effectively asks for the
|
||||||
|
non-extension permission for clipboard access.
|
||||||
|
*/
|
||||||
|
const browser = await Backend._getBrowser();
|
||||||
|
if (browser === 'firefox' || browser === 'firefox-mobile') {
|
||||||
|
return await navigator.clipboard.readText();
|
||||||
|
} else {
|
||||||
|
const clipboardPasteTarget = this.clipboardPasteTarget;
|
||||||
|
clipboardPasteTarget.value = '';
|
||||||
|
clipboardPasteTarget.focus();
|
||||||
|
document.execCommand('paste');
|
||||||
|
const result = clipboardPasteTarget.value;
|
||||||
|
clipboardPasteTarget.value = '';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onApiGetDisplayTemplatesHtml() {
|
async _onApiGetDisplayTemplatesHtml() {
|
||||||
@ -565,23 +597,68 @@ class Backend {
|
|||||||
// Command handlers
|
// Command handlers
|
||||||
|
|
||||||
async _onCommandSearch(params) {
|
async _onCommandSearch(params) {
|
||||||
const url = chrome.runtime.getURL('/bg/search.html');
|
const {mode='existingOrNewTab', query} = params || {};
|
||||||
if (!(params && params.newTab)) {
|
|
||||||
try {
|
const options = await this.getOptions(this.optionsContext);
|
||||||
const tab = await Backend._findTab(1000, (url2) => (
|
const {popupWidth, popupHeight} = options.general;
|
||||||
url2 !== null &&
|
|
||||||
url2.startsWith(url) &&
|
const baseUrl = chrome.runtime.getURL('/bg/search.html');
|
||||||
(url2.length === url.length || url2[url.length] === '?' || url2[url.length] === '#')
|
const queryParams = {mode};
|
||||||
));
|
if (query && query.length > 0) { queryParams.query = query; }
|
||||||
if (tab !== null) {
|
const queryString = new URLSearchParams(queryParams).toString();
|
||||||
await Backend._focusTab(tab);
|
const url = `${baseUrl}?${queryString}`;
|
||||||
return;
|
|
||||||
|
const isTabMatch = (url2) => {
|
||||||
|
if (url2 === null || !url2.startsWith(baseUrl)) { return false; }
|
||||||
|
const {baseUrl: baseUrl2, queryParams: queryParams2} = parseUrl(url2);
|
||||||
|
return baseUrl2 === baseUrl && (queryParams2.mode === mode || (!queryParams2.mode && mode === 'existingOrNewTab'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const openInTab = async () => {
|
||||||
|
const tab = await Backend._findTab(1000, isTabMatch);
|
||||||
|
if (tab !== null) {
|
||||||
|
await Backend._focusTab(tab);
|
||||||
|
if (queryParams.query) {
|
||||||
|
await new Promise((resolve) => chrome.tabs.sendMessage(
|
||||||
|
tab.id, {action: 'searchQueryUpdate', params: {query: queryParams.query}}, resolve
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
return true;
|
||||||
// NOP
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case 'existingOrNewTab':
|
||||||
|
try {
|
||||||
|
if (await openInTab()) { return; }
|
||||||
|
} catch (e) {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
chrome.tabs.create({url});
|
||||||
|
return;
|
||||||
|
case 'newTab':
|
||||||
|
chrome.tabs.create({url});
|
||||||
|
return;
|
||||||
|
case 'popup':
|
||||||
|
try {
|
||||||
|
// chrome.windows not supported (e.g. on Firefox mobile)
|
||||||
|
if (!isObject(chrome.windows)) { return; }
|
||||||
|
if (await openInTab()) { return; }
|
||||||
|
// if the previous popup is open in an invalid state, close it
|
||||||
|
if (this.popupWindow !== null) {
|
||||||
|
const callback = () => this.checkLastError(chrome.runtime.lastError);
|
||||||
|
chrome.windows.remove(this.popupWindow.id, callback);
|
||||||
|
}
|
||||||
|
// open new popup
|
||||||
|
this.popupWindow = await new Promise((resolve) => chrome.windows.create(
|
||||||
|
{url, width: popupWidth, height: popupHeight, type: 'popup'},
|
||||||
|
resolve
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
chrome.tabs.create({url});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCommandHelp() {
|
_onCommandHelp() {
|
||||||
|
80
ext/bg/js/clipboard-monitor.js
Normal file
80
ext/bg/js/clipboard-monitor.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
class ClipboardMonitor {
|
||||||
|
constructor() {
|
||||||
|
this.timerId = null;
|
||||||
|
this.timerToken = null;
|
||||||
|
this.interval = 250;
|
||||||
|
this.previousText = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClipboardText(_text) {
|
||||||
|
throw new Error('Override me');
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.stop();
|
||||||
|
|
||||||
|
// The token below is used as a unique identifier to ensure that a new clipboard monitor
|
||||||
|
// hasn't been started during the await call. The check below the await apiClipboardGet()
|
||||||
|
// call will exit early if the reference has changed.
|
||||||
|
const token = {};
|
||||||
|
const intervalCallback = async () => {
|
||||||
|
this.timerId = null;
|
||||||
|
|
||||||
|
let text = null;
|
||||||
|
try {
|
||||||
|
text = await apiClipboardGet();
|
||||||
|
} catch (e) {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
if (this.timerToken !== token) { return; }
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof text === 'string' &&
|
||||||
|
(text = text.trim()).length > 0 &&
|
||||||
|
text !== this.previousText
|
||||||
|
) {
|
||||||
|
this.previousText = text;
|
||||||
|
if (jpIsStringPartiallyJapanese(text)) {
|
||||||
|
this.onClipboardText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timerId = setTimeout(intervalCallback, this.interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.timerToken = token;
|
||||||
|
|
||||||
|
intervalCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.timerToken = null;
|
||||||
|
if (this.timerId !== null) {
|
||||||
|
clearTimeout(this.timerId);
|
||||||
|
this.timerId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviousText(text) {
|
||||||
|
this.previousText = text;
|
||||||
|
}
|
||||||
|
}
|
@ -30,12 +30,12 @@ function setupButtonEvents(selector, command, url) {
|
|||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
node.addEventListener('click', (e) => {
|
node.addEventListener('click', (e) => {
|
||||||
if (e.button !== 0) { return; }
|
if (e.button !== 0) { return; }
|
||||||
apiCommandExec(command, {newTab: e.ctrlKey});
|
apiCommandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}, false);
|
}, false);
|
||||||
node.addEventListener('auxclick', (e) => {
|
node.addEventListener('auxclick', (e) => {
|
||||||
if (e.button !== 1) { return; }
|
if (e.button !== 1) { return; }
|
||||||
apiCommandExec(command, {newTab: true});
|
apiCommandExec(command, {mode: 'newTab'});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
@ -266,6 +266,7 @@ function profileOptionsCreateDefaults() {
|
|||||||
return {
|
return {
|
||||||
general: {
|
general: {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
enableClipboardPopups: false,
|
||||||
resultOutputMode: 'group',
|
resultOutputMode: 'group',
|
||||||
debugInfo: false,
|
debugInfo: false,
|
||||||
maxResults: 32,
|
maxResults: 32,
|
||||||
|
@ -36,12 +36,7 @@ class DisplaySearch extends Display {
|
|||||||
this.introVisible = true;
|
this.introVisible = true;
|
||||||
this.introAnimationTimer = null;
|
this.introAnimationTimer = null;
|
||||||
|
|
||||||
this.isFirefox = false;
|
this.clipboardMonitor = new ClipboardMonitor();
|
||||||
|
|
||||||
this.clipboardMonitorTimerId = null;
|
|
||||||
this.clipboardMonitorTimerToken = null;
|
|
||||||
this.clipboardInterval = 250;
|
|
||||||
this.clipboardPreviousText = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static create() {
|
static create() {
|
||||||
@ -53,12 +48,14 @@ class DisplaySearch extends Display {
|
|||||||
async prepare() {
|
async prepare() {
|
||||||
try {
|
try {
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
this.isFirefox = await DisplaySearch._isFirefox();
|
|
||||||
|
const {queryParams: {query='', mode=''}} = parseUrl(window.location.href);
|
||||||
|
|
||||||
if (this.search !== null) {
|
if (this.search !== null) {
|
||||||
this.search.addEventListener('click', (e) => this.onSearch(e), false);
|
this.search.addEventListener('click', (e) => this.onSearch(e), false);
|
||||||
}
|
}
|
||||||
if (this.query !== null) {
|
if (this.query !== null) {
|
||||||
|
document.documentElement.dataset.searchMode = mode;
|
||||||
this.query.addEventListener('input', () => this.onSearchInput(), false);
|
this.query.addEventListener('input', () => this.onSearchInput(), false);
|
||||||
|
|
||||||
if (this.wanakanaEnable !== null) {
|
if (this.wanakanaEnable !== null) {
|
||||||
@ -69,34 +66,26 @@ class DisplaySearch extends Display {
|
|||||||
this.wanakanaEnable.checked = false;
|
this.wanakanaEnable.checked = false;
|
||||||
}
|
}
|
||||||
this.wanakanaEnable.addEventListener('change', (e) => {
|
this.wanakanaEnable.addEventListener('change', (e) => {
|
||||||
const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || '';
|
const {queryParams: {query=''}} = parseUrl(window.location.href);
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
window.wanakana.bind(this.query);
|
window.wanakana.bind(this.query);
|
||||||
this.setQuery(window.wanakana.toKana(query));
|
|
||||||
apiOptionsSet({general: {enableWanakana: true}}, this.getOptionsContext());
|
apiOptionsSet({general: {enableWanakana: true}}, this.getOptionsContext());
|
||||||
} else {
|
} else {
|
||||||
window.wanakana.unbind(this.query);
|
window.wanakana.unbind(this.query);
|
||||||
this.setQuery(query);
|
|
||||||
apiOptionsSet({general: {enableWanakana: false}}, this.getOptionsContext());
|
apiOptionsSet({general: {enableWanakana: false}}, this.getOptionsContext());
|
||||||
}
|
}
|
||||||
|
this.setQuery(query);
|
||||||
this.onSearchQueryUpdated(this.query.value, false);
|
this.onSearchQueryUpdated(this.query.value, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = DisplaySearch.getSearchQueryFromLocation(window.location.href);
|
this.setQuery(query);
|
||||||
if (query !== null) {
|
this.onSearchQueryUpdated(this.query.value, false);
|
||||||
if (this.isWanakanaEnabled()) {
|
|
||||||
this.setQuery(window.wanakana.toKana(query));
|
|
||||||
} else {
|
|
||||||
this.setQuery(query);
|
|
||||||
}
|
|
||||||
this.onSearchQueryUpdated(this.query.value, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (this.clipboardMonitorEnable !== null) {
|
if (this.clipboardMonitorEnable !== null && mode !== 'popup') {
|
||||||
if (this.options.general.enableClipboardMonitor === true) {
|
if (this.options.general.enableClipboardMonitor === true) {
|
||||||
this.clipboardMonitorEnable.checked = true;
|
this.clipboardMonitorEnable.checked = true;
|
||||||
this.startClipboardMonitor();
|
this.clipboardMonitor.start();
|
||||||
} else {
|
} else {
|
||||||
this.clipboardMonitorEnable.checked = false;
|
this.clipboardMonitorEnable.checked = false;
|
||||||
}
|
}
|
||||||
@ -106,7 +95,7 @@ class DisplaySearch extends Display {
|
|||||||
{permissions: ['clipboardRead']},
|
{permissions: ['clipboardRead']},
|
||||||
(granted) => {
|
(granted) => {
|
||||||
if (granted) {
|
if (granted) {
|
||||||
this.startClipboardMonitor();
|
this.clipboardMonitor.start();
|
||||||
apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext());
|
apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext());
|
||||||
} else {
|
} else {
|
||||||
e.target.checked = false;
|
e.target.checked = false;
|
||||||
@ -114,16 +103,20 @@ class DisplaySearch extends Display {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.stopClipboardMonitor();
|
this.clipboardMonitor.stop();
|
||||||
apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext());
|
apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
|
||||||
|
|
||||||
window.addEventListener('popstate', (e) => this.onPopState(e));
|
window.addEventListener('popstate', (e) => this.onPopState(e));
|
||||||
|
window.addEventListener('copy', (e) => this.onCopy(e));
|
||||||
|
|
||||||
|
this.clipboardMonitor.onClipboardText = (text) => this.onExternalSearchUpdate(text);
|
||||||
|
|
||||||
this.updateSearchButton();
|
this.updateSearchButton();
|
||||||
this.initClipboardMonitor();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.onError(e);
|
this.onError(e);
|
||||||
}
|
}
|
||||||
@ -159,25 +152,32 @@ class DisplaySearch extends Display {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const query = this.query.value;
|
const query = this.query.value;
|
||||||
|
|
||||||
this.queryParser.setText(query);
|
this.queryParser.setText(query);
|
||||||
const queryString = query.length > 0 ? `?query=${encodeURIComponent(query)}` : '';
|
|
||||||
window.history.pushState(null, '', `${window.location.pathname}${queryString}`);
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('query', query);
|
||||||
|
window.history.pushState(null, '', url.toString());
|
||||||
|
|
||||||
this.onSearchQueryUpdated(query, true);
|
this.onSearchQueryUpdated(query, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
onPopState() {
|
onPopState() {
|
||||||
const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || '';
|
const {queryParams: {query='', mode=''}} = parseUrl(window.location.href);
|
||||||
if (this.query !== null) {
|
document.documentElement.dataset.searchMode = mode;
|
||||||
if (this.isWanakanaEnabled()) {
|
this.setQuery(query);
|
||||||
this.setQuery(window.wanakana.toKana(query));
|
|
||||||
} else {
|
|
||||||
this.setQuery(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onSearchQueryUpdated(this.query.value, false);
|
this.onSearchQueryUpdated(this.query.value, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRuntimeMessage({action, params}, sender, callback) {
|
||||||
|
const handler = DisplaySearch._runtimeMessageHandlers.get(action);
|
||||||
|
if (typeof handler !== 'function') { return false; }
|
||||||
|
|
||||||
|
const result = handler(this, params, sender);
|
||||||
|
callback(result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
onKeyDown(e) {
|
onKeyDown(e) {
|
||||||
const key = Display.getKeyFromEvent(e);
|
const key = Display.getKeyFromEvent(e);
|
||||||
const ignoreKeys = DisplaySearch.onKeyDownIgnoreKeys;
|
const ignoreKeys = DisplaySearch.onKeyDownIgnoreKeys;
|
||||||
@ -202,6 +202,19 @@ class DisplaySearch extends Display {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCopy() {
|
||||||
|
// ignore copy from search page
|
||||||
|
this.clipboardMonitor.setPreviousText(document.getSelection().toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
onExternalSearchUpdate(text) {
|
||||||
|
this.setQuery(text);
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('query', text);
|
||||||
|
window.history.pushState(null, '', url.toString());
|
||||||
|
this.onSearchQueryUpdated(this.query.value, true);
|
||||||
|
}
|
||||||
|
|
||||||
async onSearchQueryUpdated(query, animate) {
|
async onSearchQueryUpdated(query, animate) {
|
||||||
try {
|
try {
|
||||||
const details = {};
|
const details = {};
|
||||||
@ -241,74 +254,6 @@ class DisplaySearch extends Display {
|
|||||||
this.queryParser.setOptions(this.options);
|
this.queryParser.setOptions(this.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
initClipboardMonitor() {
|
|
||||||
// ignore copy from search page
|
|
||||||
window.addEventListener('copy', () => {
|
|
||||||
this.clipboardPreviousText = document.getSelection().toString().trim();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
startClipboardMonitor() {
|
|
||||||
// The token below is used as a unique identifier to ensure that a new clipboard monitor
|
|
||||||
// hasn't been started during the await call. The check below the await this.getClipboardText()
|
|
||||||
// call will exit early if the reference has changed.
|
|
||||||
const token = {};
|
|
||||||
const intervalCallback = async () => {
|
|
||||||
this.clipboardMonitorTimerId = null;
|
|
||||||
|
|
||||||
let text = await this.getClipboardText();
|
|
||||||
if (this.clipboardMonitorTimerToken !== token) { return; }
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof text === 'string' &&
|
|
||||||
(text = text.trim()).length > 0 &&
|
|
||||||
text !== this.clipboardPreviousText
|
|
||||||
) {
|
|
||||||
this.clipboardPreviousText = text;
|
|
||||||
if (jpIsStringPartiallyJapanese(text)) {
|
|
||||||
this.setQuery(this.isWanakanaEnabled() ? window.wanakana.toKana(text) : text);
|
|
||||||
window.history.pushState(null, '', `${window.location.pathname}?query=${encodeURIComponent(text)}`);
|
|
||||||
this.onSearchQueryUpdated(this.query.value, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clipboardMonitorTimerId = setTimeout(intervalCallback, this.clipboardInterval);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.clipboardMonitorTimerToken = token;
|
|
||||||
|
|
||||||
intervalCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
stopClipboardMonitor() {
|
|
||||||
this.clipboardMonitorTimerToken = null;
|
|
||||||
if (this.clipboardMonitorTimerId !== null) {
|
|
||||||
clearTimeout(this.clipboardMonitorTimerId);
|
|
||||||
this.clipboardMonitorTimerId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getClipboardText() {
|
|
||||||
/*
|
|
||||||
Notes:
|
|
||||||
apiClipboardGet doesn't work on Firefox because document.execCommand('paste')
|
|
||||||
results in an empty string on the web extension background page.
|
|
||||||
This may be a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
|
|
||||||
Therefore, navigator.clipboard.readText() is used on Firefox.
|
|
||||||
|
|
||||||
navigator.clipboard.readText() can't be used in Chrome for two reasons:
|
|
||||||
* Requires page to be focused, else it rejects with an exception.
|
|
||||||
* When the page is focused, Chrome will request clipboard permission, despite already
|
|
||||||
being an extension with clipboard permissions. It effectively asks for the
|
|
||||||
non-extension permission for clipboard access.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
return this.isFirefox ? await navigator.clipboard.readText() : await apiClipboardGet();
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isWanakanaEnabled() {
|
isWanakanaEnabled() {
|
||||||
return this.wanakanaEnable !== null && this.wanakanaEnable.checked;
|
return this.wanakanaEnable !== null && this.wanakanaEnable.checked;
|
||||||
}
|
}
|
||||||
@ -318,8 +263,9 @@ class DisplaySearch extends Display {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setQuery(query) {
|
setQuery(query) {
|
||||||
this.query.value = query;
|
const interpretedQuery = this.isWanakanaEnabled() ? window.wanakana.toKana(query) : query;
|
||||||
this.queryParser.setText(query);
|
this.query.value = interpretedQuery;
|
||||||
|
this.queryParser.setText(interpretedQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIntroVisible(visible, animate) {
|
setIntroVisible(visible, animate) {
|
||||||
@ -394,22 +340,6 @@ class DisplaySearch extends Display {
|
|||||||
document.title = `${text} - Yomichan Search`;
|
document.title = `${text} - Yomichan Search`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getSearchQueryFromLocation(url) {
|
|
||||||
const match = /^[^?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url);
|
|
||||||
return match !== null ? decodeURIComponent(match[1]) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async _isFirefox() {
|
|
||||||
const {browser} = await apiGetEnvironmentInfo();
|
|
||||||
switch (browser) {
|
|
||||||
case 'firefox':
|
|
||||||
case 'firefox-mobile':
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplaySearch.onKeyDownIgnoreKeys = {
|
DisplaySearch.onKeyDownIgnoreKeys = {
|
||||||
@ -427,4 +357,8 @@ DisplaySearch.onKeyDownIgnoreKeys = {
|
|||||||
'Shift': []
|
'Shift': []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DisplaySearch._runtimeMessageHandlers = new Map([
|
||||||
|
['searchQueryUpdate', (self, {query}) => { self.onExternalSearchUpdate(query); }]
|
||||||
|
]);
|
||||||
|
|
||||||
DisplaySearch.instance = DisplaySearch.create();
|
DisplaySearch.instance = DisplaySearch.create();
|
||||||
|
@ -28,6 +28,22 @@ function getOptionsFullMutable() {
|
|||||||
|
|
||||||
async function formRead(options) {
|
async function formRead(options) {
|
||||||
options.general.enable = $('#enable').prop('checked');
|
options.general.enable = $('#enable').prop('checked');
|
||||||
|
const enableClipboardPopups = $('#enable-clipboard-popups').prop('checked');
|
||||||
|
if (enableClipboardPopups) {
|
||||||
|
options.general.enableClipboardPopups = await new Promise((resolve, _reject) => {
|
||||||
|
chrome.permissions.request(
|
||||||
|
{permissions: ['clipboardRead']},
|
||||||
|
(granted) => {
|
||||||
|
if (!granted) {
|
||||||
|
$('#enable-clipboard-popups').prop('checked', false);
|
||||||
|
}
|
||||||
|
resolve(granted);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
options.general.enableClipboardPopups = false;
|
||||||
|
}
|
||||||
options.general.showGuide = $('#show-usage-guide').prop('checked');
|
options.general.showGuide = $('#show-usage-guide').prop('checked');
|
||||||
options.general.compactTags = $('#compact-tags').prop('checked');
|
options.general.compactTags = $('#compact-tags').prop('checked');
|
||||||
options.general.compactGlossaries = $('#compact-glossaries').prop('checked');
|
options.general.compactGlossaries = $('#compact-glossaries').prop('checked');
|
||||||
@ -104,6 +120,7 @@ async function formRead(options) {
|
|||||||
|
|
||||||
async function formWrite(options) {
|
async function formWrite(options) {
|
||||||
$('#enable').prop('checked', options.general.enable);
|
$('#enable').prop('checked', options.general.enable);
|
||||||
|
$('#enable-clipboard-popups').prop('checked', options.general.enableClipboardPopups);
|
||||||
$('#show-usage-guide').prop('checked', options.general.showGuide);
|
$('#show-usage-guide').prop('checked', options.general.showGuide);
|
||||||
$('#compact-tags').prop('checked', options.general.compactTags);
|
$('#compact-tags').prop('checked', options.general.compactTags);
|
||||||
$('#compact-glossaries').prop('checked', options.general.compactGlossaries);
|
$('#compact-glossaries').prop('checked', options.general.compactGlossaries);
|
||||||
|
@ -25,23 +25,25 @@
|
|||||||
<p style="margin-bottom: 0;">Search your installed dictionaries by entering a Japanese expression into the field below.</p>
|
<p style="margin-bottom: 0;">Search your installed dictionaries by entering a Japanese expression into the field below.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group" style="padding-top: 20px;">
|
<div class="search-input">
|
||||||
<span title="Enable kana input method" class="input-group-text">
|
<div class="input-group" style="padding-top: 20px;">
|
||||||
<input type="checkbox" id="wanakana-enable" class="icon-checkbox" />
|
<span title="Enable kana input method" class="input-group-text">
|
||||||
<label for="wanakana-enable" class="scan-disable">あ</label>
|
<input type="checkbox" id="wanakana-enable" class="icon-checkbox" />
|
||||||
</span>
|
<label for="wanakana-enable" class="scan-disable">あ</label>
|
||||||
<span title="Enable clipboard monitor" class="input-group-text">
|
</span>
|
||||||
<input type="checkbox" id="clipboard-monitor-enable" class="icon-checkbox" />
|
<span title="Enable clipboard monitor" class="input-group-text">
|
||||||
<label for="clipboard-monitor-enable"><span class="glyphicon glyphicon-paste"></span></label>
|
<input type="checkbox" id="clipboard-monitor-enable" class="icon-checkbox" />
|
||||||
</span>
|
<label for="clipboard-monitor-enable"><span class="glyphicon glyphicon-paste"></span></label>
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form class="input-group">
|
<form class="input-group">
|
||||||
<input type="text" class="form-control" placeholder="Search for..." id="query" autofocus>
|
<input type="text" class="form-control" placeholder="Search for..." id="query" autofocus>
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<input type="submit" class="btn btn-default form-control" id="search" value="Search">
|
<input type="submit" class="btn btn-default form-control" id="search" value="Search">
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="spinner" hidden><img src="/mixed/img/spinner.gif"></div>
|
<div id="spinner" hidden><img src="/mixed/img/spinner.gif"></div>
|
||||||
|
|
||||||
@ -87,6 +89,7 @@
|
|||||||
<script src="/mixed/js/text-scanner.js"></script>
|
<script src="/mixed/js/text-scanner.js"></script>
|
||||||
|
|
||||||
<script src="/bg/js/search-query-parser.js"></script>
|
<script src="/bg/js/search-query-parser.js"></script>
|
||||||
|
<script src="/bg/js/clipboard-monitor.js"></script>
|
||||||
<script src="/bg/js/search.js"></script>
|
<script src="/bg/js/search.js"></script>
|
||||||
<script src="/bg/js/search-frontend.js"></script>
|
<script src="/bg/js/search-frontend.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -134,6 +134,10 @@
|
|||||||
<label><input type="checkbox" id="enable"> Enable content scanning</label>
|
<label><input type="checkbox" id="enable"> Enable content scanning</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox" data-hide-for-browser="firefox-mobile">
|
||||||
|
<label><input type="checkbox" id="enable-clipboard-popups"> Enable native popups when copying Japanese text</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label><input type="checkbox" id="show-usage-guide"> Show usage guide on startup</label>
|
<label><input type="checkbox" id="show-usage-guide"> Show usage guide on startup</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,6 +136,10 @@ html:root[data-yomichan-page=float] .navigation-header:not([hidden])~.navigation
|
|||||||
margin-right: 0.2em;
|
margin-right: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html:root[data-yomichan-page=search][data-search-mode=popup] .search-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Entries
|
* Entries
|
||||||
|
@ -128,6 +128,14 @@ function stringReverse(string) {
|
|||||||
return string.split('').reverse().join('').replace(/([\uDC00-\uDFFF])([\uD800-\uDBFF])/g, '$2$1');
|
return string.split('').reverse().join('').replace(/([\uDC00-\uDFFF])([\uD800-\uDBFF])/g, '$2$1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseUrl(url) {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
const baseUrl = `${parsedUrl.origin}${parsedUrl.pathname}`;
|
||||||
|
const queryParams = Array.from(parsedUrl.searchParams.entries())
|
||||||
|
.reduce((a, [k, v]) => Object.assign({}, a, {[k]: v}), {});
|
||||||
|
return {baseUrl, queryParams};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Async utilities
|
* Async utilities
|
||||||
|
Loading…
Reference in New Issue
Block a user