aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
Diffstat (limited to 'ext')
-rw-r--r--ext/bg/background.html2
-rw-r--r--ext/bg/context.html2
-rw-r--r--ext/bg/js/backend.js7
-rw-r--r--ext/bg/js/options.js7
-rw-r--r--ext/bg/js/search.js2
-rw-r--r--ext/bg/js/settings.js59
-rw-r--r--ext/bg/js/util.js16
-rw-r--r--ext/bg/lang/deinflect.json818
-rw-r--r--ext/bg/search.html2
-rw-r--r--ext/bg/settings.html36
-rw-r--r--ext/fg/css/client.css1
-rw-r--r--ext/fg/float.html2
-rw-r--r--ext/fg/js/document.js283
-rw-r--r--ext/fg/js/float.js2
-rw-r--r--ext/fg/js/frontend.js16
-rw-r--r--ext/fg/js/popup.js149
-rw-r--r--ext/fg/js/source.js40
-rw-r--r--ext/fg/js/util.js5
-rw-r--r--ext/manifest.json8
-rw-r--r--ext/mixed/js/display.js22
-rw-r--r--ext/mixed/js/extension.js53
21 files changed, 1379 insertions, 153 deletions
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;
+}