diff options
| -rw-r--r-- | ext/bg/context.html | 6 | ||||
| -rw-r--r-- | ext/bg/js/api.js | 115 | ||||
| -rw-r--r-- | ext/bg/js/backend.js | 2 | ||||
| -rw-r--r-- | ext/bg/js/context.js | 28 | ||||
| -rw-r--r-- | ext/bg/js/search.js | 17 | ||||
| -rw-r--r-- | ext/bg/js/settings.js | 11 | ||||
| -rw-r--r-- | ext/fg/js/api.js | 4 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 8 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 4 | 
9 files changed, 173 insertions, 22 deletions
| diff --git a/ext/bg/context.html b/ext/bg/context.html index 85203053..48fa463f 100644 --- a/ext/bg/context.html +++ b/ext/bg/context.html @@ -91,9 +91,9 @@                  <input type="checkbox" id="enable-search">              </div>              <div class="btn-group"> -                <button type="button" title="Search (Alt + Insert)" class="btn btn-default btn-xs action-open-search"><span class="glyphicon glyphicon-search"></span></button> -                <button type="button" title="Options" class="btn btn-default btn-xs action-open-options"><span class="glyphicon glyphicon-wrench"></span></button> -                <button type="button" title="Help" class="btn btn-default btn-xs action-open-help"><span class="glyphicon glyphicon-question-sign"></span></button> +                <a title="Search (Alt + Insert)
(Middle click to open in new tab)" class="btn btn-default btn-xs action-open-search"><span class="glyphicon glyphicon-search"></span></a> +                <a title="Options
(Middle click to open in new tab)" class="btn btn-default btn-xs action-open-options"><span class="glyphicon glyphicon-wrench"></span></a> +                <a title="Help" class="btn btn-default btn-xs action-open-help"><span class="glyphicon glyphicon-question-sign"></span></a>              </div>          </div>          <div id="full"> diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index da5ae4fe..93d9c155 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -144,24 +144,46 @@ async function apiTemplateRender(template, data, dynamic) {      }  } -async function apiCommandExec(command) { +async function apiCommandExec(command, params) {      const handlers = apiCommandExec.handlers;      if (handlers.hasOwnProperty(command)) {          const handler = handlers[command]; -        handler(); +        handler(params);      }  }  apiCommandExec.handlers = { -    search: () => { -        chrome.tabs.create({url: chrome.extension.getURL('/bg/search.html')}); +    search: async (params) => { +        const url = chrome.extension.getURL('/bg/search.html'); +        if (!(params && params.newTab)) { +            try { +                const tab = await apiFindTab(1000, (url2) => ( +                    url2 !== null && +                    url2.startsWith(url) && +                    (url2.length === url.length || url2[url.length] === '?' || url2[url.length] === '#') +                )); +                if (tab !== null) { +                    await apiFocusTab(tab); +                    return; +                } +            } catch (e) { +                // NOP +            } +        } +        chrome.tabs.create({url});      },      help: () => {          chrome.tabs.create({url: 'https://foosoft.net/projects/yomichan/'});      }, -    options: () => { -        chrome.runtime.openOptionsPage(); +    options: (params) => { +        if (!(params && params.newTab)) { +            chrome.runtime.openOptionsPage(); +        } else { +            const manifest = chrome.runtime.getManifest(); +            const url = chrome.extension.getURL(manifest.options_ui.page); +            chrome.tabs.create({url}); +        }      },      toggle: async () => { @@ -298,3 +320,84 @@ async function apiGetBrowser() {          return 'chrome';      }  } + +function apiGetTabUrl(tab) { +    return new Promise((resolve) => { +        chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => { +            let url = null; +            if (!chrome.runtime.lastError) { +                url = (response !== null && typeof response === 'object' && !Array.isArray(response) ? response.url : null); +                if (url !== null && typeof url !== 'string') { +                    url = null; +                } +            } +            resolve({tab, url}); +        }); +    }); +} + +async function apiFindTab(timeout, checkUrl) { +    // This function works around the need to have the "tabs" permission to access tab.url. +    const tabs = await new Promise((resolve) => chrome.tabs.query({}, resolve)); +    let matchPromiseResolve = null; +    const matchPromise = new Promise((resolve) => { matchPromiseResolve = resolve; }); + +    const checkTabUrl = ({tab, url}) => { +        if (checkUrl(url, tab)) { +            matchPromiseResolve(tab); +        } +    }; + +    const promises = []; +    for (const tab of tabs) { +        const promise = apiGetTabUrl(tab); +        promise.then(checkTabUrl); +        promises.push(promise); +    } + +    const racePromises = [ +        matchPromise, +        Promise.all(promises).then(() => null) +    ]; +    if (typeof timeout === 'number') { +        racePromises.push(new Promise((resolve) => setTimeout(() => resolve(null), timeout))); +    } + +    return await Promise.race(racePromises); +} + +async function apiFocusTab(tab) { +    await new Promise((resolve, reject) => { +        chrome.tabs.update(tab.id, {active: true}, () => { +            const e = chrome.runtime.lastError; +            if (e) { reject(e); } +            else { resolve(); } +        }); +    }); + +    if (!(typeof chrome.windows === 'object' && chrome.windows !== null)) { +        // Windows not supported (e.g. on Firefox mobile) +        return; +    } + +    try { +        const tabWindow = await new Promise((resolve) => { +            chrome.windows.get(tab.windowId, {}, (tabWindow) => { +                const e = chrome.runtime.lastError; +                if (e) { reject(e); } +                else { resolve(tabWindow); } +            }); +        }); +        if (!tabWindow.focused) { +            await new Promise((resolve, reject) => { +                chrome.windows.update(tab.windowId, {focused: true}, () => { +                    const e = chrome.runtime.lastError; +                    if (e) { reject(e); } +                    else { resolve(); } +                }); +            }); +        } +    } catch (e) { +        // Edge throws exception for no reason here. +    } +} diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 7560f39e..f29230a2 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -181,7 +181,7 @@ Backend.messageHandlers = {      definitionsAddable: ({definitions, modes, optionsContext}) => apiDefinitionsAddable(definitions, modes, optionsContext),      noteView: ({noteId}) => apiNoteView(noteId),      templateRender: ({template, data, dynamic}) => apiTemplateRender(template, data, dynamic), -    commandExec: ({command}) => apiCommandExec(command), +    commandExec: ({command, params}) => apiCommandExec(command, params),      audioGetUrl: ({definition, source, optionsContext}) => apiAudioGetUrl(definition, source, optionsContext),      screenshotGet: ({options}, sender) => apiScreenshotGet(options, sender),      forward: ({action, params}, sender) => apiForward(action, params, sender), diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js index a29f7aa7..8e1dbce6 100644 --- a/ext/bg/js/context.js +++ b/ext/bg/js/context.js @@ -25,6 +25,26 @@ function showExtensionInfo() {      node.textContent = `${manifest.name} v${manifest.version}`;  } +function setupButtonEvents(selector, command, url) { +    const node = $(selector); +    node.on('click', (e) => { +        if (e.button !== 0) { return; } +        apiCommandExec(command, {newTab: e.ctrlKey}); +        e.preventDefault(); +    }) +    .on('auxclick', (e) => { +        if (e.button !== 1) { return; } +        apiCommandExec(command, {newTab: true}); +        e.preventDefault(); +    }); + +    if (typeof url === 'string') { +        node.attr('href', url); +        node.attr('target', '_blank'); +        node.attr('rel', 'noopener'); +    } +} +  $(document).ready(utilAsync(() => {      showExtensionInfo(); @@ -33,9 +53,11 @@ $(document).ready(utilAsync(() => {          document.documentElement.dataset.mode = (browser === 'firefox-mobile' ? 'full' : 'mini');      }); -    $('.action-open-search').click(() => apiCommandExec('search')); -    $('.action-open-options').click(() => apiCommandExec('options')); -    $('.action-open-help').click(() => apiCommandExec('help')); +    const manifest = chrome.runtime.getManifest(); + +    setupButtonEvents('.action-open-search', 'search', chrome.extension.getURL('/bg/search.html')); +    setupButtonEvents('.action-open-options', 'options', chrome.extension.getURL(manifest.options_ui.page)); +    setupButtonEvents('.action-open-help', 'help');      const optionsContext = {          depth: 0, diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index a5a815cf..431478c9 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -114,6 +114,17 @@ class DisplaySearch extends Display {          }      } +    onRuntimeMessage({action, params}, sender, callback) { +        const handlers = DisplaySearch.runtimeMessageHandlers; +        if (handlers.hasOwnProperty(action)) { +            const handler = handlers[action]; +            const result = handler(this, params); +            callback(result); +        } else { +            return super.onRuntimeMessage({action, params}, sender, callback); +        } +    } +      getOptionsContext() {          return this.optionsContext;      } @@ -188,4 +199,10 @@ class DisplaySearch extends Display {      }  } +DisplaySearch.runtimeMessageHandlers = { +    getUrl: () => { +        return {url: window.location.href}; +    } +}; +  window.yomichan_search = DisplaySearch.create(); diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 2c77a0ed..05a0604a 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -430,9 +430,14 @@ async function onOptionsUpdate({source}) {      await formWrite(options);  } -function onMessage({action, params}) { -    if (action === 'optionsUpdate') { -        onOptionsUpdate(params); +function onMessage({action, params}, sender, callback) { +    switch (action) { +        case 'optionsUpdate': +            onOptionsUpdate(params); +            break; +        case 'getUrl': +            callback({url: window.location.href}); +            break;      }  } diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js index 2294cb8b..b0746b85 100644 --- a/ext/fg/js/api.js +++ b/ext/fg/js/api.js @@ -49,8 +49,8 @@ function apiAudioGetUrl(definition, source, optionsContext) {      return utilInvoke('audioGetUrl', {definition, source, optionsContext});  } -function apiCommandExec(command) { -    return utilInvoke('commandExec', {command}); +function apiCommandExec(command, params) { +    return utilInvoke('commandExec', {command, params});  }  function apiScreenshotGet(options) { diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 45d24329..e854f74e 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -237,8 +237,8 @@ class Frontend {          const handlers = Frontend.runtimeMessageHandlers;          if (handlers.hasOwnProperty(action)) {              const handler = handlers[action]; -            handler(this, params); -            callback(); +            const result = handler(this, params); +            callback(result);          }      } @@ -576,5 +576,9 @@ Frontend.runtimeMessageHandlers = {      popupSetVisibleOverride: (self, {visible}) => {          self.popup.setVisibleOverride(visible); +    }, + +    getUrl: () => { +        return {url: window.location.href};      }  }; diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index bab82015..b40228b0 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -175,8 +175,8 @@ class Display {          const handlers = Display.runtimeMessageHandlers;          if (handlers.hasOwnProperty(action)) {              const handler = handlers[action]; -            handler(this, params); -            callback(); +            const result = handler(this, params); +            callback(result);          }      } |