diff options
| author | Alex Yatskov <alex@foosoft.net> | 2019-09-02 11:47:14 -0700 | 
|---|---|---|
| committer | Alex Yatskov <alex@foosoft.net> | 2019-09-02 11:47:14 -0700 | 
| commit | 5347da528bd07166b4686f45440d80a77f4888a3 (patch) | |
| tree | 08bbfd0c859327ee9a08ca86afd222a222ced62b | |
| parent | da981c0b911dc5a697246006089b266fddba84a7 (diff) | |
| parent | 4ac55da7dd5354e6c3495f04583352d0d863b7b6 (diff) | |
Merge branch 'master' into testing
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | ext/bg/background.html | 2 | ||||
| -rw-r--r-- | ext/bg/context.html | 2 | ||||
| -rw-r--r-- | ext/bg/js/backend.js | 7 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 7 | ||||
| -rw-r--r-- | ext/bg/js/search.js | 2 | ||||
| -rw-r--r-- | ext/bg/js/settings.js | 59 | ||||
| -rw-r--r-- | ext/bg/js/util.js | 16 | ||||
| -rw-r--r-- | ext/bg/lang/deinflect.json | 818 | ||||
| -rw-r--r-- | ext/bg/search.html | 2 | ||||
| -rw-r--r-- | ext/bg/settings.html | 36 | ||||
| -rw-r--r-- | ext/fg/css/client.css | 1 | ||||
| -rw-r--r-- | ext/fg/float.html | 2 | ||||
| -rw-r--r-- | ext/fg/js/document.js | 283 | ||||
| -rw-r--r-- | ext/fg/js/float.js | 2 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 16 | ||||
| -rw-r--r-- | ext/fg/js/popup.js | 149 | ||||
| -rw-r--r-- | ext/fg/js/source.js | 40 | ||||
| -rw-r--r-- | ext/fg/js/util.js | 5 | ||||
| -rw-r--r-- | ext/manifest.json | 8 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 22 | ||||
| -rw-r--r-- | ext/mixed/js/extension.js | 53 | 
22 files changed, 1381 insertions, 153 deletions
| @@ -165,6 +165,7 @@ Flashcard fields can be configured with the following steps:      `{sentence}` | Sentence, quote, or phrase in which the term appears in the source content.      `{tags}` | Grammar and usage tags providing information about the term (unavailable in *grouped* mode).      `{url}` | Address of the web page in which the term appeared in. +    `{screenshot}` | Screenshot of the web page taken at the time the term was added.      #### Markers for Kanji Cards #### @@ -180,6 +181,7 @@ Flashcard fields can be configured with the following steps:      `{onyomi}` | Onyomi (Chinese reading) for the Kanji expressed as Hiragana.      `{sentence}` | Sentence, quote, or phrase in which the character appears in the source content.      `{url}` | Address of the web page in which the Kanji appeared in. +    `{screenshot}` | Screenshot of the web page taken at the time the Kanji was added.  When creating your model for Yomichan, *please make sure that you pick a unique field to be first*; fields that will  contain `{expression}` or `{character}` are ideal candidates for this. Anki does not require duplicate flashcards to be diff --git a/ext/bg/background.html b/ext/bg/background.html index 3262f2a1..5978f10f 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -10,6 +10,8 @@          <script src="/mixed/lib/jszip.min.js"></script>          <script src="/mixed/lib/wanakana.min.js"></script> +        <script src="/mixed/js/extension.js"></script> +          <script src="/bg/js/anki.js"></script>          <script src="/bg/js/api.js"></script>          <script src="/bg/js/audio.js"></script> diff --git a/ext/bg/context.html b/ext/bg/context.html index 01b4fb30..198ccd42 100644 --- a/ext/bg/context.html +++ b/ext/bg/context.html @@ -32,6 +32,8 @@          <script src="/mixed/lib/jquery.min.js"></script>          <script src="/mixed/lib/bootstrap-toggle/bootstrap-toggle.min.js"></script> +        <script src="/mixed/js/extension.js"></script> +          <script src="/bg/js/api.js"></script>          <script src="/bg/js/options.js"></script>          <script src="/bg/js/util.js"></script> diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index d49286d0..d95cb82d 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -57,9 +57,10 @@ class Backend {              this.anki = new AnkiNull();          } +        const callback = () => this.checkLastError(chrome.runtime.lastError);          chrome.tabs.query({}, tabs => {              for (const tab of tabs) { -                chrome.tabs.sendMessage(tab.id, {action: 'optionsSet', params: options}, () => null); +                chrome.tabs.sendMessage(tab.id, {action: 'optionsSet', params: options}, callback);              }          });      } @@ -147,6 +148,10 @@ class Backend {              chrome.browserAction.setBadgeText({text});          }      } + +    checkLastError(e) { +        // NOP +    }  }  window.yomichan_backend = new Backend(); diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 29d8a215..7d993987 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -199,6 +199,10 @@ function optionsSetDefaults(options) {              popupHeight: 250,              popupHorizontalOffset: 0,              popupVerticalOffset: 10, +            popupHorizontalOffset2: 10, +            popupVerticalOffset2: 0, +            popupHorizontalTextPosition: 'below', +            popupVerticalTextPosition: 'before',              showGuide: true,              compactTags: false,              compactGlossaries: false, @@ -214,7 +218,8 @@ function optionsSetDefaults(options) {              autoHideResults: false,              delay: 20,              length: 10, -            modifier: 'shift' +            modifier: 'shift', +            deepDomScan: false          },          dictionaries: {}, diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index f08f22da..a3382398 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -25,7 +25,7 @@ class DisplaySearch extends Display {          this.query = $('#query').on('input', this.onSearchInput.bind(this));          this.intro = $('#intro'); -        this.dependencies = {...this.dependencies, ...{docRangeFromPoint, docSentenceExtract}}; +        this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract});          window.wanakana.bind(this.query.get(0));      } diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 75082f3e..f5d669b2 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -32,10 +32,14 @@ async function formRead() {      optionsNew.general.showAdvanced = $('#show-advanced-options').prop('checked');      optionsNew.general.maxResults = parseInt($('#max-displayed-results').val(), 10);      optionsNew.general.popupDisplayMode = $('#popup-display-mode').val(); +    optionsNew.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val(); +    optionsNew.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val();      optionsNew.general.popupWidth = parseInt($('#popup-width').val(), 10);      optionsNew.general.popupHeight = parseInt($('#popup-height').val(), 10);      optionsNew.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0);      optionsNew.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10); +    optionsNew.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0); +    optionsNew.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10);      optionsNew.general.customPopupCss = $('#custom-popup-css').val();      optionsNew.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); @@ -43,6 +47,7 @@ async function formRead() {      optionsNew.scanning.selectText = $('#select-matched-text').prop('checked');      optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked');      optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); +    optionsNew.scanning.deepDomScan = $('#deep-dom-scan').prop('checked');      optionsNew.scanning.delay = parseInt($('#scan-delay').val(), 10);      optionsNew.scanning.length = parseInt($('#scan-length').val(), 10);      optionsNew.scanning.modifier = $('#scan-modifier-key').val(); @@ -116,7 +121,7 @@ async function formMainDictionaryOptionsPopulate(options) {      select.append($('<option class="text-muted" value="">Not selected</option>'));      let mainDictionary = ''; -    for (const dictRow of await utilDatabaseSummarize()) { +    for (const dictRow of toIterable(await utilDatabaseSummarize())) {          if (dictRow.sequenced) {              select.append($(`<option value="${dictRow.title}">${dictRow.title}</option>`));              if (dictRow.title === options.general.mainDictionary) { @@ -168,10 +173,14 @@ async function onReady() {      $('#show-advanced-options').prop('checked', options.general.showAdvanced);      $('#max-displayed-results').val(options.general.maxResults);      $('#popup-display-mode').val(options.general.popupDisplayMode); +    $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition); +    $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition);      $('#popup-width').val(options.general.popupWidth);      $('#popup-height').val(options.general.popupHeight);      $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset);      $('#popup-vertical-offset').val(options.general.popupVerticalOffset); +    $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2); +    $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2);      $('#custom-popup-css').val(options.general.customPopupCss);      $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); @@ -179,6 +188,7 @@ async function onReady() {      $('#select-matched-text').prop('checked', options.scanning.selectText);      $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric);      $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); +    $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan);      $('#scan-delay').val(options.scanning.delay);      $('#scan-length').val(options.scanning.length);      $('#scan-modifier-key').val(options.scanning.modifier); @@ -314,12 +324,12 @@ async function dictionaryGroupsPopulate(options) {      const dictGroups = $('#dict-groups').empty();      const dictWarning = $('#dict-warning').hide(); -    const dictRows = await utilDatabaseSummarize(); +    const dictRows = toIterable(await utilDatabaseSummarize());      if (dictRows.length === 0) {          dictWarning.show();      } -    for (const dictRow of dictRowsSort(dictRows, options)) { +    for (const dictRow of toIterable(dictRowsSort(dictRows, options))) {          const dictOptions = options.dictionaries[dictRow.title] || {              enabled: false,              priority: 0, @@ -581,26 +591,25 @@ async function onAnkiFieldTemplatesReset(e) {   */  async function getBrowser() { -    if (typeof chrome !== "undefined") { -        if (typeof browser !== "undefined") { -            try { -                const info = await browser.runtime.getBrowserInfo(); -                if (info.name === "Fennec") { -                    return "firefox-mobile"; -                } -            } catch (e) { } -            return "firefox"; -        } else { -            return "chrome"; -        } +    if (EXTENSION_IS_BROWSER_EDGE) { +        return 'edge'; +    } +    if (typeof browser !== 'undefined') { +        try { +            const info = await browser.runtime.getBrowserInfo(); +            if (info.name === 'Fennec') { +                return 'firefox-mobile'; +            } +        } catch (e) { } +        return 'firefox';      } else { -        return "edge"; +        return 'chrome';      }  }  function storageBytesToLabeledString(size) {      const base = 1000; -    const labels = ["bytes", "KB", "MB", "GB"]; +    const labels = ['bytes', 'KB', 'MB', 'GB'];      let labelIndex = 0;      while (size >= base) {          size /= base; @@ -620,14 +629,14 @@ storageEstimate.mostRecent = null;  async function storageInfoInitialize() {      const browser = await getBrowser(); -    const container = document.querySelector("#storage-info"); -    container.setAttribute("data-browser", browser); +    const container = document.querySelector('#storage-info'); +    container.setAttribute('data-browser', browser);      await storageShowInfo(); -    container.classList.remove("storage-hidden"); +    container.classList.remove('storage-hidden'); -    document.querySelector("#storage-refresh").addEventListener('click', () => storageShowInfo(), false); +    document.querySelector('#storage-refresh').addEventListener('click', () => storageShowInfo(), false);  }  async function storageUpdateStats() { @@ -637,8 +646,8 @@ async function storageUpdateStats() {      const valid = (estimate !== null);      if (valid) { -        document.querySelector("#storage-usage").textContent = storageBytesToLabeledString(estimate.usage); -        document.querySelector("#storage-quota").textContent = storageBytesToLabeledString(estimate.quota); +        document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage); +        document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota);      }      storageUpdateStats.isUpdating = false; @@ -650,8 +659,8 @@ async function storageShowInfo() {      storageSpinnerShow(true);      const valid = await storageUpdateStats(); -    document.querySelector("#storage-use").classList.toggle("storage-hidden", !valid); -    document.querySelector("#storage-error").classList.toggle("storage-hidden", valid); +    document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid); +    document.querySelector('#storage-error').classList.toggle('storage-hidden', valid);      storageSpinnerShow(false);  } diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 34b06ddb..3dc7c900 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -87,6 +87,20 @@ function utilDatabasePurge() {      return utilBackend().translator.database.purge();  } -function utilDatabaseImport(data, progress, exceptions) { +async function utilDatabaseImport(data, progress, exceptions) { +    // Edge cannot read data on the background page due to the File object +    // being created from a different window. Read on the same page instead. +    if (EXTENSION_IS_BROWSER_EDGE) { +        data = await utilReadFile(data); +    }      return utilBackend().translator.database.importDictionary(data, progress, exceptions);  } + +function utilReadFile(file) { +    return new Promise((resolve, reject) => { +        const reader = new FileReader(); +        reader.onload = () => resolve(reader.result); +        reader.onerror = () => reject(reader.error); +        reader.readAsBinaryString(file); +    }); +} diff --git a/ext/bg/lang/deinflect.json b/ext/bg/lang/deinflect.json index 7ee00b2f..7a68ea71 100644 --- a/ext/bg/lang/deinflect.json +++ b/ext/bg/lang/deinflect.json @@ -784,6 +784,150 @@              "rulesOut": [                  "adj-i"              ] +        }, +        { +            "kanaIn": "のたもうたら", +            "kanaOut": "のたまう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "いったら", +            "kanaOut": "いく", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "おうたら", +            "kanaOut": "おう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "こうたら", +            "kanaOut": "こう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "そうたら", +            "kanaOut": "そう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "とうたら", +            "kanaOut": "とう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "行ったら", +            "kanaOut": "行く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "逝ったら", +            "kanaOut": "逝く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "往ったら", +            "kanaOut": "往く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "請うたら", +            "kanaOut": "請う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "乞うたら", +            "kanaOut": "乞う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "恋うたら", +            "kanaOut": "恋う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "問うたら", +            "kanaOut": "問う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "負うたら", +            "kanaOut": "負う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "沿うたら", +            "kanaOut": "沿う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "添うたら", +            "kanaOut": "添う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "副うたら", +            "kanaOut": "副う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "厭うたら", +            "kanaOut": "厭う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ]          }      ],      "-tari": [ @@ -891,6 +1035,150 @@              "rulesOut": [                  "adj-i"              ] +        }, +        { +            "kanaIn": "のたもうたり", +            "kanaOut": "のたまう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "いったり", +            "kanaOut": "いく", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "おうたり", +            "kanaOut": "おう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "こうたり", +            "kanaOut": "こう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "そうたり", +            "kanaOut": "そう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "とうたり", +            "kanaOut": "とう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "行ったり", +            "kanaOut": "行く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "逝ったり", +            "kanaOut": "逝く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "往ったり", +            "kanaOut": "往く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "請うたり", +            "kanaOut": "請う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "乞うたり", +            "kanaOut": "乞う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "恋うたり", +            "kanaOut": "恋う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "問うたり", +            "kanaOut": "問う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "負うたり", +            "kanaOut": "負う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "沿うたり", +            "kanaOut": "沿う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "添うたり", +            "kanaOut": "添う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "副うたり", +            "kanaOut": "副う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "厭うたり", +            "kanaOut": "厭う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ]          }      ],      "-te": [ @@ -998,6 +1286,150 @@              "rulesOut": [                  "v5"              ] +        }, +        { +            "kanaIn": "のたもうて", +            "kanaOut": "のたまう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "いって", +            "kanaOut": "いく", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "おうて", +            "kanaOut": "おう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "こうて", +            "kanaOut": "こう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "そうて", +            "kanaOut": "そう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "とうて", +            "kanaOut": "とう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "行って", +            "kanaOut": "行く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "逝って", +            "kanaOut": "逝く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "往って", +            "kanaOut": "往く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "請うて", +            "kanaOut": "請う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "乞うて", +            "kanaOut": "乞う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "恋うて", +            "kanaOut": "恋う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "問うて", +            "kanaOut": "問う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "負うて", +            "kanaOut": "負う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "沿うて", +            "kanaOut": "沿う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "添うて", +            "kanaOut": "添う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "副うて", +            "kanaOut": "副う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "厭うて", +            "kanaOut": "厭う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ]          }      ],      "-zu": [ @@ -1251,6 +1683,16 @@              ]          },          { +            "kanaIn": "させる", +            "kanaOut": "す", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        {              "kanaIn": "たせる",              "kanaOut": "つ",              "rulesIn": [ @@ -1498,6 +1940,14 @@              ]          },          { +            "kanaIn": "き", +            "kanaOut": "くる", +            "rulesIn": [], +            "rulesOut": [ +                "vk" +            ] +        }, +        {              "kanaIn": "ぎ",              "kanaOut": "ぎる",              "rulesIn": [], @@ -1881,6 +2331,16 @@              ]          },          { +            "kanaIn": "される", +            "kanaOut": "す", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        {              "kanaIn": "たれる",              "kanaOut": "つ",              "rulesIn": [ @@ -1929,12 +2389,10 @@              "rulesOut": [                  "v5"              ] -        } -    ], -    "passive or causative": [ +        },          { -            "kanaIn": "される", -            "kanaOut": "す", +            "kanaIn": "られる", +            "kanaOut": "る",              "rulesIn": [                  "v1"              ], @@ -2048,6 +2506,150 @@              "rulesOut": [                  "adj-i"              ] +        }, +        { +            "kanaIn": "のたもうた", +            "kanaOut": "のたまう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "いった", +            "kanaOut": "いく", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "おうた", +            "kanaOut": "おう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "こうた", +            "kanaOut": "こう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "そうた", +            "kanaOut": "そう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "とうた", +            "kanaOut": "とう", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "行った", +            "kanaOut": "行く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "逝った", +            "kanaOut": "逝く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "往った", +            "kanaOut": "往く", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "請うた", +            "kanaOut": "請う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "乞うた", +            "kanaOut": "乞う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "恋うた", +            "kanaOut": "恋う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "問うた", +            "kanaOut": "問う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "負うた", +            "kanaOut": "負う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "沿うた", +            "kanaOut": "沿う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "添うた", +            "kanaOut": "添う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "副うた", +            "kanaOut": "副う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "厭うた", +            "kanaOut": "厭う", +            "rulesIn": [], +            "rulesOut": [ +                "v5" +            ]          }      ],      "polite": [ @@ -2674,7 +3276,6 @@              ],              "rulesOut": [                  "v1", -                "v5",                  "vk"              ]          }, @@ -2787,5 +3388,210 @@                  "vs"              ]          } +    ], +    "causative passive": [ +        { +            "kanaIn": "かされる", +            "kanaOut": "く", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "がされる", +            "kanaOut": "ぐ", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "たされる", +            "kanaOut": "つ", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "なされる", +            "kanaOut": "ぬ", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "ばされる", +            "kanaOut": "ぶ", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "まされる", +            "kanaOut": "む", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "らされる", +            "kanaOut": "る", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "わされる", +            "kanaOut": "う", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "v5" +            ] +        } +    ], +    "-toku": [ +        { +            "kanaIn": "いとく", +            "kanaOut": "く", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "いどく", +            "kanaOut": "ぐ", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "きとく", +            "kanaOut": "くる", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "vk" +            ] +        }, +        { +            "kanaIn": "しとく", +            "kanaOut": "す", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "しとく", +            "kanaOut": "する", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "vs" +            ] +        }, +        { +            "kanaIn": "っとく", +            "kanaOut": "う", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "っとく", +            "kanaOut": "つ", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "っとく", +            "kanaOut": "る", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "んどく", +            "kanaOut": "ぬ", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "んどく", +            "kanaOut": "ぶ", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "んどく", +            "kanaOut": "む", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "v5" +            ] +        }, +        { +            "kanaIn": "とく", +            "kanaOut": "る", +            "rulesIn": [ +                "v5" +            ], +            "rulesOut": [ +                "v1", +                "vk" +            ] +        }      ]  } diff --git a/ext/bg/search.html b/ext/bg/search.html index ce156578..05c0daab 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -37,6 +37,8 @@          <script src="/mixed/lib/jquery.min.js"></script>          <script src="/mixed/lib/wanakana.min.js"></script> +        <script src="/mixed/js/extension.js"></script> +          <script src="/bg/js/api.js"></script>          <script src="/bg/js/audio.js"></script>          <script src="/bg/js/dictionary.js"></script> diff --git a/ext/bg/settings.html b/ext/bg/settings.html index c6677018..cc140023 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -107,6 +107,28 @@                      </select>                  </div> +                <div class="form-group"> +                    <div class="row"> +                        <div class="col-xs-6"> +                            <label for="popup-display-mode">Popup position for horizontal text</label> +                            <select class="form-control" id="popup-horizontal-text-position"> +                                <option value="below">Below text</option> +                                <option value="above">Above text</option> +                            </select> +                        </div> +                        <div class="col-xs-6"> +                            <label for="popup-display-mode">Popup position for vertical text</label> +                            <select class="form-control" id="popup-vertical-text-position"> +                                <option value="default">Same as for horizontal text</option> +                                <option value="before">Before text reading direction</option> +                                <option value="after">After text reading direction</option> +                                <option value="left">Left of text</option> +                                <option value="right">Right of text</option> +                            </select> +                        </div> +                    </div> +                </div> +                  <div class="form-group options-advanced">                      <label for="audio-playback-volume">Audio playback volume (percent)</label>                      <input type="number" min="0" max="100" id="audio-playback-volume" class="form-control"> @@ -134,6 +156,14 @@                  </div>                  <div class="form-group options-advanced"> +                    <label>Popup offset for vertical text (horizontal, vertical; in pixels)</label> +                    <div class="row"> +                        <div class="col-xs-6"><input type="number" min="0" id="popup-horizontal-offset2" class="form-control"></div> +                        <div class="col-xs-6"><input type="number" min="0" id="popup-vertical-offset2" class="form-control"></div> +                    </div> +                </div> + +                <div class="form-group options-advanced">                      <label for="custom-popup-css">Custom popup CSS</label>                      <div><textarea autocomplete="off" spellcheck="false" wrap="soft" id="custom-popup-css" class="form-control"></textarea></div>                  </div> @@ -162,6 +192,10 @@                      <label><input type="checkbox" id="auto-hide-results"> Automatically hide results</label>                  </div> +                <div class="checkbox options-advanced"> +                    <label><input type="checkbox" id="deep-dom-scan"> Deep DOM scan</label> +                </div> +                  <div class="form-group options-advanced">                      <label for="scan-delay">Scan delay (in milliseconds)</label>                      <input type="number" min="1" id="scan-delay" class="form-control"> @@ -399,6 +433,8 @@          <script src="/mixed/lib/bootstrap/js/bootstrap.min.js"></script>          <script src="/mixed/lib/handlebars.min.js"></script> +        <script src="/mixed/js/extension.js"></script> +          <script src="/bg/js/anki.js"></script>          <script src="/bg/js/api.js"></script>          <script src="/bg/js/dictionary.js"></script> diff --git a/ext/fg/css/client.css b/ext/fg/css/client.css index a9b8e025..a2b06d0f 100644 --- a/ext/fg/css/client.css +++ b/ext/fg/css/client.css @@ -26,6 +26,7 @@ iframe#yomichan-float {      resize: both;      visibility: hidden;      z-index: 2147483647; +    box-sizing: border-box;  }  iframe#yomichan-float.yomichan-float-full-width { diff --git a/ext/fg/float.html b/ext/fg/float.html index 07f2d58b..0133e653 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -34,6 +34,8 @@          <script src="/mixed/lib/jquery.min.js"></script>          <script src="/mixed/lib/wanakana.min.js"></script> +        <script src="/mixed/js/extension.js"></script> +          <script src="/fg/js/api.js"></script>          <script src="/fg/js/util.js"></script>          <script src="/fg/js/document.js"></script> diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index 86396a8a..bd876e5d 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -17,77 +17,110 @@   */ -const IS_FIREFOX = /Firefox/.test(navigator.userAgent); +const REGEX_TRANSPARENT_COLOR = /rgba\s*\([^\)]*,\s*0(?:\.0+)?\s*\)/; -function docOffsetCalc(element) { -    const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; -    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft; - -    const clientTop = document.documentElement.clientTop || document.body.clientTop || 0; -    const clientLeft = document.documentElement.clientLeft || document.body.clientLeft || 0; +function docSetImposterStyle(style, propertyName, value) { +    style.setProperty(propertyName, value, 'important'); +} -    const rect = element.getBoundingClientRect(); -    const top  = Math.round(rect.top +  scrollTop - clientTop); -    const left = Math.round(rect.left + scrollLeft - clientLeft); +function docImposterCreate(element, isTextarea) { +    const elementStyle = window.getComputedStyle(element); +    const elementRect = element.getBoundingClientRect(); +    const documentRect = document.documentElement.getBoundingClientRect(); +    const left = elementRect.left - documentRect.left; +    const top = elementRect.top - documentRect.top; + +    // Container +    const container = document.createElement('div'); +    const containerStyle = container.style; +    docSetImposterStyle(containerStyle, 'all', 'initial'); +    docSetImposterStyle(containerStyle, 'position', 'absolute'); +    docSetImposterStyle(containerStyle, 'left', '0'); +    docSetImposterStyle(containerStyle, 'top', '0'); +    docSetImposterStyle(containerStyle, 'width', `${documentRect.width}px`); +    docSetImposterStyle(containerStyle, 'height', `${documentRect.height}px`); +    docSetImposterStyle(containerStyle, 'overflow', 'hidden'); +    docSetImposterStyle(containerStyle, 'opacity', '0'); + +    docSetImposterStyle(containerStyle, 'pointer-events', 'none'); +    docSetImposterStyle(containerStyle, 'z-index', '2147483646'); + +    // Imposter +    const imposter = document.createElement('div'); +    const imposterStyle = imposter.style; -    return {top, left}; -} +    imposter.innerText = element.value; -function docImposterCreate(element) { -    const styleProps = window.getComputedStyle(element); -    const stylePairs = []; -    for (const key of styleProps) { -        stylePairs.push(`${key}: ${styleProps[key]};`); +    for (let i = 0, ii = elementStyle.length; i < ii; ++i) { +        const property = elementStyle[i]; +        docSetImposterStyle(imposterStyle, property, elementStyle.getPropertyValue(property)); +    } +    docSetImposterStyle(imposterStyle, 'position', 'absolute'); +    docSetImposterStyle(imposterStyle, 'top', `${top}px`); +    docSetImposterStyle(imposterStyle, 'left', `${left}px`); +    docSetImposterStyle(imposterStyle, 'margin', '0'); +    docSetImposterStyle(imposterStyle, 'pointer-events', 'auto'); + +    if (isTextarea) { +        if (elementStyle.overflow === 'visible') { +            docSetImposterStyle(imposterStyle, 'overflow', 'auto'); +        } +    } else { +        docSetImposterStyle(imposterStyle, 'overflow', 'hidden'); +        docSetImposterStyle(imposterStyle, 'white-space', 'nowrap'); +        docSetImposterStyle(imposterStyle, 'line-height', elementStyle.height);      } -    const offset = docOffsetCalc(element); -    const imposter = document.createElement('div'); -    imposter.className = 'yomichan-imposter'; -    imposter.innerText = element.value; -    imposter.style.cssText = stylePairs.join('\n'); -    imposter.style.position = 'absolute'; -    imposter.style.top = `${offset.top}px`; -    imposter.style.left = `${offset.left}px`; -    imposter.style.opacity = 0; -    imposter.style.zIndex = 2147483646; -    if (element.nodeName === 'TEXTAREA' && styleProps.overflow === 'visible') { -        imposter.style.overflow = 'auto'; +    container.appendChild(imposter); +    document.body.appendChild(container); + +    // Adjust size +    const imposterRect = imposter.getBoundingClientRect(); +    if (imposterRect.width !== elementRect.width || imposterRect.height !== elementRect.height) { +        const width = parseFloat(elementStyle.width) + (elementRect.width - imposterRect.width); +        const height = parseFloat(elementStyle.height) + (elementRect.height - imposterRect.height); +        docSetImposterStyle(imposterStyle, 'width', `${width}px`); +        docSetImposterStyle(imposterStyle, 'height', `${height}px`);      } -    document.body.appendChild(imposter);      imposter.scrollTop = element.scrollTop;      imposter.scrollLeft = element.scrollLeft; -    return imposter; -} - -function docImposterDestroy() { -    for (const element of document.getElementsByClassName('yomichan-imposter')) { -        element.parentNode.removeChild(element); -    } +    return [imposter, container];  } -function docRangeFromPoint(point) { -    const element = document.elementFromPoint(point.x, point.y); +function docRangeFromPoint({x, y}, options) { +    const elements = document.elementsFromPoint(x, y);      let imposter = null; -    if (element) { +    let imposterContainer = null; +    if (elements.length > 0) { +        const element = elements[0];          switch (element.nodeName) {              case 'IMG':              case 'BUTTON':                  return new TextSourceElement(element);              case 'INPUT': +                [imposter, imposterContainer] = docImposterCreate(element, false); +                break;              case 'TEXTAREA': -                imposter = docImposterCreate(element); +                [imposter, imposterContainer] = docImposterCreate(element, true);                  break;          }      } -    const range = document.caretRangeFromPoint(point.x, point.y); -    if (imposter !== null) { -        imposter.style.zIndex = -2147483646; +    const range = caretRangeFromPointExt(x, y, options.scanning.deepDomScan ? elements : []); +    if (range !== null) { +        if (imposter !== null) { +            docSetImposterStyle(imposterContainer.style, 'z-index', '-2147483646'); +            docSetImposterStyle(imposter.style, 'pointer-events', 'none'); +        } +        return new TextSourceRange(range, '', imposterContainer); +    } else { +        if (imposterContainer !== null) { +            imposterContainer.parentNode.removeChild(imposterContainer); +        } +        return null;      } - -    return range !== null && isPointInRange(point, range) ? new TextSourceRange(range) : null;  }  function docSentenceExtract(source, extent) { @@ -161,32 +194,158 @@ function docSentenceExtract(source, extent) {      };  } -function isPointInRange(point, range) { -    if (IS_FIREFOX) { -        // Always return true on Firefox due to an issue where range.getClientRects() -        // does not return a correct set of rects for characters at the beginning of a line. -        return true; +function isPointInRange(x, y, range) { +    // Require a text node to start +    if (range.startContainer.nodeType !== Node.TEXT_NODE) { +        return false;      } -    const y = point.y - 2; -    for (const rect of range.getClientRects()) { -        if (y <= rect.bottom) { +    // Scan forward +    const nodePre = range.endContainer; +    const offsetPre = range.endOffset; +    try { +        const {node, offset, content} = TextSourceRange.seekForward(range.endContainer, range.endOffset, 1); +        range.setEnd(node, offset); + +        if (!isWhitespace(content) && isPointInAnyRect(x, y, range.getClientRects())) {              return true;          } +    } finally { +        range.setEnd(nodePre, offsetPre); +    } + +    // Scan backward +    const {node, offset, content} = TextSourceRange.seekBackward(range.startContainer, range.startOffset, 1); +    range.setStart(node, offset); + +    if (!isWhitespace(content) && isPointInAnyRect(x, y, range.getClientRects())) { +        // This purposefully leaves the starting offset as modified and sets the range length to 0. +        range.setEnd(node, offset); +        return true;      } +    // No match +    return false; +} + +function isWhitespace(string) { +    return string.trim().length === 0; +} + +function isPointInAnyRect(x, y, rects) { +    for (const rect of rects) { +        if (isPointInRect(x, y, rect)) { +            return true; +        } +    }      return false;  } -if (typeof document.caretRangeFromPoint !== 'function') { -    document.caretRangeFromPoint = (x, y) => { -        const position = document.caretPositionFromPoint(x, y); -        if (position && position.offsetNode && position.offsetNode.nodeType === Node.TEXT_NODE) { +function isPointInRect(x, y, rect) { +    return ( +        x >= rect.left && x < rect.right && +        y >= rect.top && y < rect.bottom); +} + +const caretRangeFromPoint = (() => { +    if (typeof document.caretRangeFromPoint === 'function') { +        // Chrome, Edge +        return (x, y) => document.caretRangeFromPoint(x, y); +    } + +    if (typeof document.caretPositionFromPoint === 'function') { +        // Firefox +        return (x, y) => { +            const position = document.caretPositionFromPoint(x, y); +            const node = position.offsetNode; +            if (node === null) { +                return null; +            } +              const range = document.createRange(); -            range.setStart(position.offsetNode, position.offset); -            range.setEnd(position.offsetNode, position.offset); +            const offset = (node.nodeType === Node.TEXT_NODE ? position.offset : 0); +            range.setStart(node, offset); +            range.setEnd(node, offset);              return range; +        }; +    } + +    // No support +    return () => null; +})(); + +function caretRangeFromPointExt(x, y, elements) { +    const modifications = []; +    try { +        let i = 0; +        let startContinerPre = null; +        while (true) { +            const range = caretRangeFromPoint(x, y); +            if (range === null) { +                return null; +            } + +            const startContainer = range.startContainer; +            if (startContinerPre !== startContainer) { +                if (isPointInRange(x, y, range)) { +                    return range; +                } +                startContinerPre = startContainer; +            } + +            i = disableTransparentElement(elements, i, modifications); +            if (i < 0) { +                return null; +            }          } -        return null; -    }; +    } finally { +        if (modifications.length > 0) { +            restoreElementStyleModifications(modifications); +        } +    } +} + +function disableTransparentElement(elements, i, modifications) { +    while (true) { +        if (i >= elements.length) { +            return -1; +        } + +        const element = elements[i++]; +        if (isElementTransparent(element)) { +            const style = element.hasAttribute('style') ? element.getAttribute('style') : null; +            modifications.push({element, style}); +            element.style.pointerEvents = 'none'; +            return i; +        } +    } +} + +function restoreElementStyleModifications(modifications) { +    for (const {element, style} of modifications) { +        if (style === null) { +            element.removeAttribute('style'); +        } else { +            element.setAttribute('style', style); +        } +    } +} + +function isElementTransparent(element) { +    if ( +        element === document.body || +        element === document.documentElement +    ) { +        return false; +    } +    const style = window.getComputedStyle(element); +    return ( +        parseFloat(style.opacity) < 0 || +        style.visibility === 'hidden' || +        (style.backgroundImage === 'none' && isColorTransparent(style.backgroundColor)) +    ); +} + +function isColorTransparent(cssColor) { +    return REGEX_TRANSPARENT_COLOR.test(cssColor);  } diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 090839a1..c0ec8a15 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -23,7 +23,7 @@ class DisplayFloat extends Display {          this.autoPlayAudioTimer = null;          this.styleNode = null; -        this.dependencies = {...this.dependencies, ...{docRangeFromPoint, docSentenceExtract}}; +        this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract});          $(window).on('message', utilAsync(this.onMessage.bind(this)));      } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 3c5f2ac8..8a5c48d0 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -285,7 +285,7 @@ class Frontend {              return;          } -        const textSource = docRangeFromPoint(point); +        const textSource = docRangeFromPoint(point, this.options);          let hideResults = !textSource || !textSource.containsPoint(point);          let searched = false;          let success = false; @@ -301,16 +301,21 @@ class Frontend {          } catch (e) {              if (window.yomichan_orphaned) {                  if (textSource && this.options.scanning.modifier !== 'none') { -                    this.popup.showOrphaned(textSource.getRect(), this.options); +                    this.popup.showOrphaned( +                        textSource.getRect(), +                        textSource.getWritingMode(), +                        this.options +                    );                  }              } else {                  this.onError(e);              }          } finally { +            if (textSource !== null) { +                textSource.cleanup(); +            }              if (hideResults && this.options.scanning.autoHideResults) {                  this.searchClear(); -            } else { -                docImposterDestroy();              }              this.pendingLookup = false; @@ -332,6 +337,7 @@ class Frontend {          const url = window.location.href;          this.popup.termsShow(              textSource.getRect(), +            textSource.getWritingMode(),              definitions,              this.options,              {sentence, url, focus} @@ -357,6 +363,7 @@ class Frontend {          const url = window.location.href;          this.popup.kanjiShow(              textSource.getRect(), +            textSource.getWritingMode(),              definitions,              this.options,              {sentence, url, focus} @@ -371,7 +378,6 @@ class Frontend {      }      searchClear() { -        docImposterDestroy();          this.popup.hide();          this.popup.clearAutoPlayTimer(); diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 18dc0386..86ce575d 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -48,59 +48,132 @@ class Popup {          return this.injected;      } -    async show(elementRect, options) { +    async show(elementRect, writingMode, options) {          await this.inject(options); -        const containerStyle = window.getComputedStyle(this.container); -        const containerHeight = parseInt(containerStyle.height); -        const containerWidth = parseInt(containerStyle.width); +        const optionsGeneral = options.general; +        const container = this.container; +        const containerRect = container.getBoundingClientRect(); +        const getPosition = ( +            writingMode === 'horizontal-tb' || optionsGeneral.popupVerticalTextPosition === 'default' ? +            Popup.getPositionForHorizontalText : +            Popup.getPositionForVerticalText +        ); -        const limitX = document.body.clientWidth; -        const limitY = window.innerHeight; +        const [x, y, width, height, below] = getPosition( +            elementRect, +            Math.max(containerRect.width, optionsGeneral.popupWidth), +            Math.max(containerRect.height, optionsGeneral.popupHeight), +            document.body.clientWidth, +            window.innerHeight, +            optionsGeneral, +            writingMode +        ); -        let x = elementRect.left + options.general.popupHorizontalOffset; -        let width = Math.max(containerWidth, options.general.popupWidth); -        const overflowX = Math.max(x + width - limitX, 0); +        container.classList.toggle('yomichan-float-full-width', optionsGeneral.popupDisplayMode === 'full-width'); +        container.classList.toggle('yomichan-float-above', !below); +        container.style.left = `${x}px`; +        container.style.top = `${y}px`; +        container.style.width = `${width}px`; +        container.style.height = `${height}px`; +        container.style.visibility = 'visible'; +    } + +    static getPositionForHorizontalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral) { +        let x = elementRect.left + optionsGeneral.popupHorizontalOffset; +        const overflowX = Math.max(x + width - maxWidth, 0);          if (overflowX > 0) {              if (x >= overflowX) {                  x -= overflowX;              } else { -                width = limitX; +                width = maxWidth;                  x = 0;              }          } -        let above = false; -        let y = 0; -        let height = Math.max(containerHeight, options.general.popupHeight); -        const yBelow = elementRect.bottom + options.general.popupVerticalOffset; -        const yAbove = elementRect.top - options.general.popupVerticalOffset; -        const overflowBelow = Math.max(yBelow + height - limitY, 0); -        const overflowAbove = Math.max(height - yAbove, 0); -        if (overflowBelow > 0 || overflowAbove > 0) { -            if (overflowBelow < overflowAbove) { -                height = Math.max(height - overflowBelow, 0); -                y = yBelow; +        const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); + +        const verticalOffset = optionsGeneral.popupVerticalOffset; +        const [y, h, below] = Popup.limitGeometry( +            elementRect.top - verticalOffset, +            elementRect.bottom + verticalOffset, +            height, +            maxHeight, +            preferBelow +        ); + +        return [x, y, width, h, below]; +    } + +    static getPositionForVerticalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral, writingMode) { +        const preferRight = Popup.isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode); +        const horizontalOffset = optionsGeneral.popupHorizontalOffset2; +        const verticalOffset = optionsGeneral.popupVerticalOffset2; + +        const [x, w] = Popup.limitGeometry( +            elementRect.left - horizontalOffset, +            elementRect.right + horizontalOffset, +            width, +            maxWidth, +            preferRight +        ); +        const [y, h, below] = Popup.limitGeometry( +            elementRect.bottom - verticalOffset, +            elementRect.top + verticalOffset, +            height, +            maxHeight, +            true +        ); +        return [x, y, w, h, below]; +    } + +    static isVerticalTextPopupOnRight(positionPreference, writingMode) { +        switch (positionPreference) { +            case 'before': +                return !Popup.isWritingModeLeftToRight(writingMode); +            case 'after': +                return Popup.isWritingModeLeftToRight(writingMode); +            case 'left': +                return false; +            case 'right': +                return true; +        } +    } + +    static isWritingModeLeftToRight(writingMode) { +        switch (writingMode) { +            case 'vertical-lr': +            case 'sideways-lr': +                return true; +            default: +                return false; +        } +    } + +    static limitGeometry(positionBefore, positionAfter, size, limit, preferAfter) { +        let after = preferAfter; +        let position = 0; +        const overflowBefore = Math.max(0, size - positionBefore); +        const overflowAfter = Math.max(0, positionAfter + size - limit); +        if (overflowAfter > 0 || overflowBefore > 0) { +            if (overflowAfter < overflowBefore) { +                size = Math.max(0, size - overflowAfter); +                position = positionAfter; +                after = true;              } else { -                height = Math.max(height - overflowAbove, 0); -                y = Math.max(yAbove - height, 0); -                above = true; +                size = Math.max(0, size - overflowBefore); +                position = Math.max(0, positionBefore - size); +                after = false;              }          } else { -            y = yBelow; +            position = preferAfter ? positionAfter : positionBefore - size;          } -        this.container.classList.toggle('yomichan-float-full-width', options.general.popupDisplayMode === 'full-width'); -        this.container.classList.toggle('yomichan-float-above', above); -        this.container.style.left = `${x}px`; -        this.container.style.top = `${y}px`; -        this.container.style.width = `${width}px`; -        this.container.style.height = `${height}px`; -        this.container.style.visibility = 'visible'; +        return [position, size, after];      } -    async showOrphaned(elementRect, options) { -        await this.show(elementRect, options); +    async showOrphaned(elementRect, writingMode, options) { +        await this.show(elementRect, writingMode, options);          this.invokeApi('orphaned');      } @@ -136,13 +209,13 @@ class Popup {          return contained;      } -    async termsShow(elementRect, definitions, options, context) { -        await this.show(elementRect, options); +    async termsShow(elementRect, writingMode, definitions, options, context) { +        await this.show(elementRect, writingMode, options);          this.invokeApi('termsShow', {definitions, options, context});      } -    async kanjiShow(elementRect, definitions, options, context) { -        await this.show(elementRect, options); +    async kanjiShow(elementRect, writingMode, definitions, options, context) { +        await this.show(elementRect, writingMode, options);          this.invokeApi('kanjiShow', {definitions, options, context});      } diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index a360b331..e724488d 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -25,13 +25,20 @@ const IGNORE_TEXT_PATTERN = /\u200c/;   */  class TextSourceRange { -    constructor(range, content='') { +    constructor(range, content, imposterContainer) {          this.range = range;          this.content = content; +        this.imposterContainer = imposterContainer;      }      clone() { -        return new TextSourceRange(this.range.cloneRange(), this.content); +        return new TextSourceRange(this.range.cloneRange(), this.content, this.imposterContainer); +    } + +    cleanup() { +        if (this.imposterContainer !== null && this.imposterContainer.parentNode !== null) { +            this.imposterContainer.parentNode.removeChild(this.imposterContainer); +        }      }      text() { @@ -61,6 +68,10 @@ class TextSourceRange {          return this.range.getBoundingClientRect();      } +    getWritingMode() { +        return TextSourceRange.getElementWritingMode(TextSourceRange.getParentElement(this.range.startContainer)); +    } +      getPaddedRect() {          const range = this.range.cloneRange();          const startOffset = range.startOffset; @@ -204,6 +215,23 @@ class TextSourceRange {          return state.remainder > 0;      } + +    static getParentElement(node) { +        while (node !== null && node.nodeType !== Node.ELEMENT_NODE) { +            node = node.parentNode; +        } +        return node; +    } + +    static getElementWritingMode(element) { +        if (element === null) { +            return 'horizontal-tb'; +        } + +        const style = window.getComputedStyle(element); +        const writingMode = style.writingMode; +        return typeof writingMode === 'string' ? writingMode : 'horizontal-tb'; +    }  } @@ -221,6 +249,10 @@ class TextSourceElement {          return new TextSourceElement(this.element, this.content);      } +    cleanup() { +        // NOP +    } +      text() {          return this.content;      } @@ -267,6 +299,10 @@ class TextSourceElement {          return this.element.getBoundingClientRect();      } +    getWritingMode() { +        return 'horizontal-tb'; +    } +      select() {          // NOP      } diff --git a/ext/fg/js/util.js b/ext/fg/js/util.js index 954b3988..7518beb5 100644 --- a/ext/fg/js/util.js +++ b/ext/fg/js/util.js @@ -27,6 +27,7 @@ function utilInvoke(action, params={}) {      return new Promise((resolve, reject) => {          try {              chrome.runtime.sendMessage({action, params}, (response) => { +                utilCheckLastError(chrome.runtime.lastError);                  if (response !== null && typeof response === 'object') {                      if (response.error) {                          reject(response.error); @@ -43,3 +44,7 @@ function utilInvoke(action, params={}) {          }      });  } + +function utilCheckLastError(e) { +    // NOP +} diff --git a/ext/manifest.json b/ext/manifest.json index 5903eb6e..b49a3123 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -1,7 +1,7 @@  {      "manifest_version": 2,      "name": "Yomichan (testing)", -    "version": "1.7.4", +    "version": "1.7.5",      "description": "Japanese dictionary with Anki integration (testing)",      "icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"}, @@ -11,10 +11,14 @@      },      "author": "Alex Yatskov", -    "background": {"page": "bg/background.html"}, +    "background": { +        "page": "bg/background.html", +        "persistent": true +    },      "content_scripts": [{          "matches": ["http://*/*", "https://*/*", "file://*/*"],          "js": [ +            "mixed/js/extension.js",              "fg/js/api.js",              "fg/js/document.js",              "fg/js/popup.js", diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index a2707bd0..ebf56897 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -80,20 +80,26 @@ class Display {              const {docRangeFromPoint, docSentenceExtract} = this.dependencies;              const clickedElement = $(e.target); -            const textSource = docRangeFromPoint({x: e.clientX, y: e.clientY}); +            const textSource = docRangeFromPoint({x: e.clientX, y: e.clientY}, this.options);              if (textSource === null) {                  return false;              } -            textSource.setEndOffset(this.options.scanning.length); -            const {definitions, length} = await apiTermsFind(textSource.text()); -            if (definitions.length === 0) { -                return false; -            } +            let definitions, length, sentence; +            try { +                textSource.setEndOffset(this.options.scanning.length); -            textSource.setEndOffset(length); +                ({definitions, length} = await apiTermsFind(textSource.text())); +                if (definitions.length === 0) { +                    return false; +                } -            const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); +                textSource.setEndOffset(length); + +                sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); +            } finally { +                textSource.cleanup(); +            }              const context = {                  source: { diff --git a/ext/mixed/js/extension.js b/ext/mixed/js/extension.js new file mode 100644 index 00000000..d7085e5b --- /dev/null +++ b/ext/mixed/js/extension.js @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019  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 <http://www.gnu.org/licenses/>. + */ + + +function toIterable(value) { +    if (typeof Symbol !== 'undefined' && typeof value[Symbol.iterator] !== 'undefined') { +        return value; +    } + +    const array = JSON.parse(JSON.stringify(value)); +    return Array.isArray(array) ? array : []; +} + +function extensionHasChrome() { +    try { +        return typeof chrome === 'object' && chrome !== null; +    } catch (e) { +        return false; +    } +} + +function extensionHasBrowser() { +    try { +        return typeof browser === 'object' && browser !== null; +    } catch (e) { +        return false; +    } +} + +const EXTENSION_IS_BROWSER_EDGE = ( +    extensionHasBrowser() && +    (!extensionHasChrome() || (typeof chrome.runtime === 'undefined' && typeof browser.runtime !== 'undefined')) +); + +if (EXTENSION_IS_BROWSER_EDGE) { +    // Edge does not have chrome defined. +    chrome = browser; +} |