Merge pull request #333 from siikamiika/native-popup-windows

Native popup windows
This commit is contained in:
siikamiika 2020-02-10 11:12:36 +02:00 committed by GitHub
commit cbfae2b9d7
14 changed files with 312 additions and 160 deletions

View File

@ -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>

View File

@ -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;

View File

@ -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"],

View File

@ -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) => {

View File

@ -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,6 +536,22 @@ class Backend {
} }
async _onApiClipboardGet() { async _onApiClipboardGet() {
/*
Notes:
document.execCommand('paste') doesn't work on Firefox.
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.
*/
const browser = await Backend._getBrowser();
if (browser === 'firefox' || browser === 'firefox-mobile') {
return await navigator.clipboard.readText();
} else {
const clipboardPasteTarget = this.clipboardPasteTarget; const clipboardPasteTarget = this.clipboardPasteTarget;
clipboardPasteTarget.value = ''; clipboardPasteTarget.value = '';
clipboardPasteTarget.focus(); clipboardPasteTarget.focus();
@ -529,6 +560,7 @@ class Backend {
clipboardPasteTarget.value = ''; clipboardPasteTarget.value = '';
return result; return result;
} }
}
async _onApiGetDisplayTemplatesHtml() { async _onApiGetDisplayTemplatesHtml() {
const url = chrome.runtime.getURL('/mixed/display-templates.html'); const url = chrome.runtime.getURL('/mixed/display-templates.html');
@ -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; }
const queryString = new URLSearchParams(queryParams).toString();
const url = `${baseUrl}?${queryString}`;
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) { if (tab !== null) {
await Backend._focusTab(tab); await Backend._focusTab(tab);
return; if (queryParams.query) {
await new Promise((resolve) => chrome.tabs.sendMessage(
tab.id, {action: 'searchQueryUpdate', params: {query: queryParams.query}}, resolve
));
} }
return true;
}
};
switch (mode) {
case 'existingOrNewTab':
try {
if (await openInTab()) { return; }
} catch (e) { } catch (e) {
// NOP // NOP
} }
}
chrome.tabs.create({url}); 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;
}
} }
_onCommandHelp() { _onCommandHelp() {

View 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;
}
}

View File

@ -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);

View File

@ -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,

View File

@ -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);
if (query !== null) {
if (this.isWanakanaEnabled()) {
this.setQuery(window.wanakana.toKana(query));
} else {
this.setQuery(query); this.setQuery(query);
}
this.onSearchQueryUpdated(this.query.value, false); this.onSearchQueryUpdated(this.query.value, false);
} }
} if (this.clipboardMonitorEnable !== null && mode !== 'popup') {
if (this.clipboardMonitorEnable !== null) {
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,23 +152,30 @@ 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(window.wanakana.toKana(query));
} else {
this.setQuery(query); 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) {
@ -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();

View File

@ -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);

View File

@ -25,6 +25,7 @@
<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="search-input">
<div class="input-group" style="padding-top: 20px;"> <div class="input-group" style="padding-top: 20px;">
<span title="Enable kana input method" class="input-group-text"> <span title="Enable kana input method" class="input-group-text">
<input type="checkbox" id="wanakana-enable" class="icon-checkbox" /> <input type="checkbox" id="wanakana-enable" class="icon-checkbox" />
@ -42,6 +43,7 @@
<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>

View File

@ -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>

View File

@ -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

View File

@ -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