Compare commits
10 Commits
a370b46fae
...
528a63fdac
Author | SHA1 | Date | |
---|---|---|---|
528a63fdac | |||
2f13cf5d2a | |||
a528c72107 | |||
991fa8189a | |||
|
752d73418b | ||
|
4abbc707a5 | ||
|
c93682f677 | ||
|
096bde44ee | ||
|
a47cbc39e2 | ||
|
9ef7f9d383 |
14
README.md
14
README.md
@ -1,5 +1,8 @@
|
|||||||
# Yomichan
|
# Yomichan
|
||||||
|
|
||||||
|
*Note: this project is no longer maintained. Please see [this
|
||||||
|
post](https://foosoft.net/posts/sunsetting-the-yomichan-project/) for more information.*
|
||||||
|
|
||||||
Yomichan turns your web browser into a tool for building Japanese language literacy by helping you to decipher texts
|
Yomichan turns your web browser into a tool for building Japanese language literacy by helping you to decipher texts
|
||||||
which would be otherwise too difficult tackle. This extension is similar to
|
which would be otherwise too difficult tackle. This extension is similar to
|
||||||
[Rikaichamp](https://addons.mozilla.org/en-US/firefox/addon/rikaichamp/) for Firefox and
|
[Rikaichamp](https://addons.mozilla.org/en-US/firefox/addon/rikaichamp/) for Firefox and
|
||||||
@ -43,16 +46,7 @@ New changes are initially introduced into the *testing* version, and after some
|
|||||||
relatively bug free, they will be promoted to the *stable* version. If you are technically savvy and don't mind
|
relatively bug free, they will be promoted to the *stable* version. If you are technically savvy and don't mind
|
||||||
submitting issues on GitHub, try the *testing* version; otherwise, the *stable* version will be your best bet.
|
submitting issues on GitHub, try the *testing* version; otherwise, the *stable* version will be your best bet.
|
||||||
|
|
||||||
* **Google Chrome**
|
*Note: [Yomichan is no longer available for download](/posts/passing-the-torch-to-yomitan).*
|
||||||
([stable](https://chrome.google.com/webstore/detail/yomichan/ogmnaimimemjmbakcfefmnahgdfhfami) or [testing](https://chrome.google.com/webstore/detail/yomichan-testing/bcknnfebhefllbjhagijobjklocakpdm)) \
|
|
||||||
[![](img/chrome-web-store.png)](https://chrome.google.com/webstore/detail/yomichan/ogmnaimimemjmbakcfefmnahgdfhfami)
|
|
||||||
|
|
||||||
* **Mozilla Firefox**
|
|
||||||
([stable](https://addons.mozilla.org/en-US/firefox/addon/yomichan/) or [testing](https://github.com/FooSoft/yomichan/releases)<sup>*</sup>) \
|
|
||||||
[![](img/firefox-marketplace.png)](https://addons.mozilla.org/en-US/firefox/addon/yomichan/) \
|
|
||||||
<sup>*</sup>Unlike Chrome, Firefox does not allow extensions meant for testing to be hosted in the marketplace.
|
|
||||||
You will have to download a desired version and side-load it yourself. You only need to do this once and will get
|
|
||||||
updates automatically.
|
|
||||||
|
|
||||||
## Dictionaries
|
## Dictionaries
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"manifest": {
|
"manifest": {
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Yomichan",
|
"name": "Yomichan",
|
||||||
"version": "22.9.9.2",
|
"version": "22.10.23.0",
|
||||||
"description": "Japanese dictionary with Anki integration",
|
"description": "Japanese dictionary with Anki integration",
|
||||||
"author": "Alex Yatskov",
|
"author": "Alex Yatskov",
|
||||||
"icons": {
|
"icons": {
|
||||||
@ -259,7 +259,7 @@
|
|||||||
{
|
{
|
||||||
"action": "set",
|
"action": "set",
|
||||||
"path": ["browser_specific_settings", "gecko", "update_url"],
|
"path": ["browser_specific_settings", "gecko", "update_url"],
|
||||||
"value": "https://foosoft.net/projects/yomichan/dl/updates.json"
|
"value": "https://raw.githubusercontent.com/FooSoft/yomichan/metadata/updates.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"excludeFiles": [
|
"excludeFiles": [
|
||||||
|
@ -41,9 +41,10 @@ class JsonSchemaAjv {
|
|||||||
|
|
||||||
validate(data) {
|
validate(data) {
|
||||||
if (this._validate(data)) { return; }
|
if (this._validate(data)) { return; }
|
||||||
const {errors} = this._validate(data);
|
const {errors} = this._validate;
|
||||||
const message = errors.map((e) => e.toString()).join('\n');
|
const error = new Error('Schema validation failed');
|
||||||
throw new Error(message);
|
error.data = JSON.parse(JSON.stringify(errors));
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"addons": {
|
|
||||||
"alex.testing@foosoft.net": {
|
|
||||||
"updates": [
|
|
||||||
{
|
|
||||||
"version": "21.10.31.1",
|
|
||||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/21.10.31.1/yomichan_testing-21.10.31.1-an+fx.xpi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "22.2.2.0",
|
|
||||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.2.2.0/yomichan_testing-22.2.2.0-an+fx.xpi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "22.4.4.0",
|
|
||||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.4.4.0/yomichan_testing-22.4.4.0-an+fx.xpi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "22.6.6.0",
|
|
||||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.6.6.0/a708116f79104891acbd-22.6.6.0.xpi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "22.9.9.0",
|
|
||||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.9.9.0/a708116f79104891acbd-22.9.9.0.xpi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "22.9.9.1",
|
|
||||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.9.9.1/a708116f79104891acbd-22.9.9.1.xpi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "22.9.9.2",
|
|
||||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.9.9.2/a708116f79104891acbd-22.9.9.2.xpi"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,6 +11,7 @@
|
|||||||
<link rel="icon" type="image/png" href="/images/icon48.png" sizes="48x48">
|
<link rel="icon" type="image/png" href="/images/icon48.png" sizes="48x48">
|
||||||
<link rel="icon" type="image/png" href="/images/icon64.png" sizes="64x64">
|
<link rel="icon" type="image/png" href="/images/icon64.png" sizes="64x64">
|
||||||
<link rel="icon" type="image/png" href="/images/icon128.png" sizes="128x128">
|
<link rel="icon" type="image/png" href="/images/icon128.png" sizes="128x128">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/background.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@ -61,7 +62,7 @@
|
|||||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
|
https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
|
||||||
-->
|
-->
|
||||||
<!-- [html-validate-disable close-order] -->
|
<!-- [html-validate-disable close-order] -->
|
||||||
<div id="clipboard-image-paste-target" contenteditable="true">
|
<div id="clipboard-rich-content-paste-target" contenteditable="true">
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
29
ext/css/background.css
Normal file
29
ext/css/background.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Yomichan Authors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* stylelint-disable declaration-no-important */
|
||||||
|
#clipboard-rich-content-paste-target * {
|
||||||
|
background-image: none !important;
|
||||||
|
list-style-image: none !important;
|
||||||
|
content: none !important;
|
||||||
|
cursor: auto !important;
|
||||||
|
border-image-source: none !important;
|
||||||
|
offset-path: none !important;
|
||||||
|
-webkit-mask-image: none !important;
|
||||||
|
mask-image: none !important;
|
||||||
|
}
|
||||||
|
/* stylelint-enable declaration-no-important */
|
@ -86,6 +86,7 @@ h1 {
|
|||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
.search-button {
|
.search-button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["freq"],
|
"const": "freq",
|
||||||
"description": "Type of data. \"freq\" corresponds to frequency information."
|
"description": "Type of data. \"freq\" corresponds to frequency information."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"tag": {
|
"tag": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["br"]
|
"const": "br"
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/structuredContentData"
|
"$ref": "#/definitions/structuredContentData"
|
||||||
@ -364,7 +364,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["text"]
|
"const": "text"
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -381,7 +381,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["structured-content"]
|
"const": "structured-content"
|
||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"$ref": "#/definitions/structuredContent",
|
"$ref": "#/definitions/structuredContent",
|
||||||
@ -398,7 +398,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["image"]
|
"const": "image"
|
||||||
},
|
},
|
||||||
"path": {
|
"path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{},
|
{},
|
||||||
{"enum": ["freq"]},
|
{"const": "freq"},
|
||||||
{
|
{
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
@ -81,7 +81,7 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{},
|
{},
|
||||||
{"enum": ["pitch"]},
|
{"const": "pitch"},
|
||||||
{
|
{
|
||||||
"type": ["object"],
|
"type": ["object"],
|
||||||
"description": "Pitch accent information for the term.",
|
"description": "Pitch accent information for the term.",
|
||||||
|
@ -38,7 +38,14 @@
|
|||||||
* wanakana
|
* wanakana
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class controls the core logic of the extension, including API calls
|
||||||
|
* and various forms of communication between browser tabs and external applications.
|
||||||
|
*/
|
||||||
class Backend {
|
class Backend {
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this._japaneseUtil = new JapaneseUtil(wanakana);
|
this._japaneseUtil = new JapaneseUtil(wanakana);
|
||||||
this._environment = new Environment();
|
this._environment = new Environment();
|
||||||
@ -53,7 +60,7 @@ class Backend {
|
|||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
document: (typeof document === 'object' && document !== null ? document : null),
|
document: (typeof document === 'object' && document !== null ? document : null),
|
||||||
pasteTargetSelector: '#clipboard-paste-target',
|
pasteTargetSelector: '#clipboard-paste-target',
|
||||||
imagePasteTargetSelector: '#clipboard-image-paste-target'
|
richContentPasteTargetSelector: '#clipboard-rich-content-paste-target'
|
||||||
});
|
});
|
||||||
this._clipboardMonitor = new ClipboardMonitor({
|
this._clipboardMonitor = new ClipboardMonitor({
|
||||||
japaneseUtil: this._japaneseUtil,
|
japaneseUtil: this._japaneseUtil,
|
||||||
@ -145,6 +152,10 @@ class Backend {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the instance.
|
||||||
|
* @returns {Promise<void>} A promise which is resolved when initialization completes.
|
||||||
|
*/
|
||||||
prepare() {
|
prepare() {
|
||||||
if (this._preparePromise === null) {
|
if (this._preparePromise === null) {
|
||||||
const promise = this._prepareInternal();
|
const promise = this._prepareInternal();
|
||||||
@ -596,7 +607,7 @@ class Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _onApiClipboardGet() {
|
async _onApiClipboardGet() {
|
||||||
return this._clipboardReader.getText();
|
return this._clipboardReader.getText(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onApiGetDisplayTemplatesHtml() {
|
async _onApiGetDisplayTemplatesHtml() {
|
||||||
@ -1773,7 +1784,7 @@ class Backend {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (clipboardDetails !== null && clipboardDetails.text) {
|
if (clipboardDetails !== null && clipboardDetails.text) {
|
||||||
clipboardText = await this._clipboardReader.getText();
|
clipboardText = await this._clipboardReader.getText(false);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.push(serializeError(e));
|
errors.push(serializeError(e));
|
||||||
|
@ -15,13 +15,29 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to generate `fetch()` requests on the background page
|
||||||
|
* with additional controls over anonymity and error handling.
|
||||||
|
*/
|
||||||
class RequestBuilder {
|
class RequestBuilder {
|
||||||
|
/**
|
||||||
|
* A progress callback for a fetch read.
|
||||||
|
* @callback ProgressCallback
|
||||||
|
* @param {boolean} complete Whether or not the data has been completely read.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this._onBeforeSendHeadersExtraInfoSpec = ['blocking', 'requestHeaders', 'extraHeaders'];
|
this._onBeforeSendHeadersExtraInfoSpec = ['blocking', 'requestHeaders', 'extraHeaders'];
|
||||||
this._textEncoder = new TextEncoder();
|
this._textEncoder = new TextEncoder();
|
||||||
this._ruleIds = new Set();
|
this._ruleIds = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the instance.
|
||||||
|
*/
|
||||||
async prepare() {
|
async prepare() {
|
||||||
try {
|
try {
|
||||||
await this._clearDynamicRules();
|
await this._clearDynamicRules();
|
||||||
@ -30,7 +46,14 @@ class RequestBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs an anonymized fetch request, which strips the `Cookie` header and adjust the `Origin` header.
|
||||||
|
* @param {string} url The URL to fetch.
|
||||||
|
* @param {RequestInit} init The initialization parameters passed to the `fetch` function.
|
||||||
|
* @returns {Promise<Response>} The response of the `fetch` call.
|
||||||
|
*/
|
||||||
async fetchAnonymous(url, init) {
|
async fetchAnonymous(url, init) {
|
||||||
|
fetch(1, 2);
|
||||||
if (isObject(chrome.declarativeNetRequest)) {
|
if (isObject(chrome.declarativeNetRequest)) {
|
||||||
return await this._fetchAnonymousDeclarative(url, init);
|
return await this._fetchAnonymousDeclarative(url, init);
|
||||||
}
|
}
|
||||||
@ -42,6 +65,12 @@ class RequestBuilder {
|
|||||||
return await this._fetchInternal(url, init, headerModifications);
|
return await this._fetchInternal(url, init, headerModifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the array buffer body of a fetch response, with an optional `onProgress` callback.
|
||||||
|
* @param {Response} response The response of a `fetch` call.
|
||||||
|
* @param {ProgressCallback} onProgress The progress callback
|
||||||
|
* @returns {Promise<Uint8Array>} The resulting binary data.
|
||||||
|
*/
|
||||||
static async readFetchResponseArrayBuffer(response, onProgress) {
|
static async readFetchResponseArrayBuffer(response, onProgress) {
|
||||||
let reader;
|
let reader;
|
||||||
try {
|
try {
|
||||||
|
@ -19,7 +19,13 @@
|
|||||||
* AnkiUtil
|
* AnkiUtil
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class controls communication with Anki via the AnkiConnect plugin.
|
||||||
|
*/
|
||||||
class AnkiConnect {
|
class AnkiConnect {
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this._enabled = false;
|
this._enabled = false;
|
||||||
this._server = null;
|
this._server = null;
|
||||||
@ -29,30 +35,59 @@ class AnkiConnect {
|
|||||||
this._apiKey = null;
|
this._apiKey = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the URL of the AnkiConnect server.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
get server() {
|
get server() {
|
||||||
return this._server;
|
return this._server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns the URL of the AnkiConnect server.
|
||||||
|
* @param {string} value The new server URL to assign.
|
||||||
|
*/
|
||||||
set server(value) {
|
set server(value) {
|
||||||
this._server = value;
|
this._server = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets whether or not server communication is enabled.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
get enabled() {
|
get enabled() {
|
||||||
return this._enabled;
|
return this._enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether or not server communication is enabled.
|
||||||
|
* @param {boolean} value The enabled state.
|
||||||
|
*/
|
||||||
set enabled(value) {
|
set enabled(value) {
|
||||||
this._enabled = value;
|
this._enabled = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the API key used when connecting to AnkiConnect.
|
||||||
|
* The value will be `null` if no API key is used.
|
||||||
|
* @type {?string}
|
||||||
|
*/
|
||||||
get apiKey() {
|
get apiKey() {
|
||||||
return this._apiKey;
|
return this._apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the API key used when connecting to AnkiConnect.
|
||||||
|
* @param {?string} value The API key to use, or `null` if no API key should be used.
|
||||||
|
*/
|
||||||
set apiKey(value) {
|
set apiKey(value) {
|
||||||
this._apiKey = value;
|
this._apiKey = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a connection to AnkiConnect can be established.
|
||||||
|
* @returns {Promise<boolean>} `true` if the connection was made, `false` otherwise.
|
||||||
|
*/
|
||||||
async isConnected() {
|
async isConnected() {
|
||||||
try {
|
try {
|
||||||
await this._invoke('version');
|
await this._invoke('version');
|
||||||
@ -62,6 +97,10 @@ class AnkiConnect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the AnkiConnect API version number.
|
||||||
|
* @returns {Promise<number>} The version number
|
||||||
|
*/
|
||||||
async getVersion() {
|
async getVersion() {
|
||||||
if (!this._enabled) { return null; }
|
if (!this._enabled) { return null; }
|
||||||
await this._checkVersion();
|
await this._checkVersion();
|
||||||
|
@ -39,7 +39,7 @@ class ClipboardMonitor extends EventDispatcher {
|
|||||||
|
|
||||||
let text = null;
|
let text = null;
|
||||||
try {
|
try {
|
||||||
text = await this._clipboardReader.getText();
|
text = await this._clipboardReader.getText(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// NOP
|
// NOP
|
||||||
}
|
}
|
||||||
|
@ -28,15 +28,15 @@ class ClipboardReader {
|
|||||||
* @param {object} details Details about how to set up the instance.
|
* @param {object} details Details about how to set up the instance.
|
||||||
* @param {?Document} details.document The Document object to be used, or null for no support.
|
* @param {?Document} details.document The Document object to be used, or null for no support.
|
||||||
* @param {?string} details.pasteTargetSelector The selector for the paste target element.
|
* @param {?string} details.pasteTargetSelector The selector for the paste target element.
|
||||||
* @param {?string} details.imagePasteTargetSelector The selector for the image paste target element.
|
* @param {?string} details.richContentPasteTargetSelector The selector for the rich content paste target element.
|
||||||
*/
|
*/
|
||||||
constructor({document=null, pasteTargetSelector=null, imagePasteTargetSelector=null}) {
|
constructor({document=null, pasteTargetSelector=null, richContentPasteTargetSelector=null}) {
|
||||||
this._document = document;
|
this._document = document;
|
||||||
this._browser = null;
|
this._browser = null;
|
||||||
this._pasteTarget = null;
|
this._pasteTarget = null;
|
||||||
this._pasteTargetSelector = pasteTargetSelector;
|
this._pasteTargetSelector = pasteTargetSelector;
|
||||||
this._imagePasteTarget = null;
|
this._richContentPasteTarget = null;
|
||||||
this._imagePasteTargetSelector = imagePasteTargetSelector;
|
this._richContentPasteTargetSelector = richContentPasteTargetSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,13 +56,14 @@ class ClipboardReader {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the text in the clipboard.
|
* Gets the text in the clipboard.
|
||||||
|
* @param {boolean} useRichText Whether or not to use rich text for pasting, when possible.
|
||||||
* @returns {string} A string containing the clipboard text.
|
* @returns {string} A string containing the clipboard text.
|
||||||
* @throws {Error} Error if not supported.
|
* @throws {Error} Error if not supported.
|
||||||
*/
|
*/
|
||||||
async getText() {
|
async getText(useRichText) {
|
||||||
/*
|
/*
|
||||||
Notes:
|
Notes:
|
||||||
document.execCommand('paste') doesn't work on Firefox.
|
document.execCommand('paste') sometimes doesn't work on Firefox.
|
||||||
See: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
|
See: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
|
||||||
Therefore, navigator.clipboard.readText() is used on Firefox.
|
Therefore, navigator.clipboard.readText() is used on Firefox.
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ class ClipboardReader {
|
|||||||
being an extension with clipboard permissions. It effectively asks for the
|
being an extension with clipboard permissions. It effectively asks for the
|
||||||
non-extension permission for clipboard access.
|
non-extension permission for clipboard access.
|
||||||
*/
|
*/
|
||||||
if (this._isFirefox()) {
|
if (this._isFirefox() && !useRichText) {
|
||||||
try {
|
try {
|
||||||
return await navigator.clipboard.readText();
|
return await navigator.clipboard.readText();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -86,15 +87,15 @@ class ClipboardReader {
|
|||||||
throw new Error('Clipboard reading not supported in this context');
|
throw new Error('Clipboard reading not supported in this context');
|
||||||
}
|
}
|
||||||
|
|
||||||
let target = this._pasteTarget;
|
if (useRichText) {
|
||||||
if (target === null) {
|
const target = this._getRichContentPasteTarget();
|
||||||
target = document.querySelector(this._pasteTargetSelector);
|
target.focus();
|
||||||
if (target === null) {
|
document.execCommand('paste');
|
||||||
throw new Error('Clipboard paste target does not exist');
|
const result = target.textContent;
|
||||||
}
|
this._clearRichContent(target);
|
||||||
this._pasteTarget = target;
|
return result;
|
||||||
}
|
} else {
|
||||||
|
const target = this._getPasteTarget();
|
||||||
target.value = '';
|
target.value = '';
|
||||||
target.focus();
|
target.focus();
|
||||||
document.execCommand('paste');
|
document.execCommand('paste');
|
||||||
@ -102,6 +103,7 @@ class ClipboardReader {
|
|||||||
target.value = '';
|
target.value = '';
|
||||||
return (typeof result === 'string' ? result : '');
|
return (typeof result === 'string' ? result : '');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the first image in the clipboard.
|
* Gets the first image in the clipboard.
|
||||||
@ -143,23 +145,12 @@ class ClipboardReader {
|
|||||||
throw new Error('Clipboard reading not supported in this context');
|
throw new Error('Clipboard reading not supported in this context');
|
||||||
}
|
}
|
||||||
|
|
||||||
let target = this._imagePasteTarget;
|
const target = this._getRichContentPasteTarget();
|
||||||
if (target === null) {
|
|
||||||
target = document.querySelector(this._imagePasteTargetSelector);
|
|
||||||
if (target === null) {
|
|
||||||
throw new Error('Clipboard paste target does not exist');
|
|
||||||
}
|
|
||||||
this._imagePasteTarget = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
target.focus();
|
target.focus();
|
||||||
document.execCommand('paste');
|
document.execCommand('paste');
|
||||||
const image = target.querySelector('img[src^="data:"]');
|
const image = target.querySelector('img[src^="data:"]');
|
||||||
const result = (image !== null ? image.getAttribute('src') : null);
|
const result = (image !== null ? image.getAttribute('src') : null);
|
||||||
for (const image2 of target.querySelectorAll('img')) {
|
this._clearRichContent(target);
|
||||||
image2.removeAttribute('src');
|
|
||||||
}
|
|
||||||
target.textContent = '';
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,4 +168,28 @@ class ClipboardReader {
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getPasteTarget() {
|
||||||
|
if (this._pasteTarget === null) { this._pasteTarget = this._findPasteTarget(this._pasteTargetSelector); }
|
||||||
|
return this._pasteTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getRichContentPasteTarget() {
|
||||||
|
if (this._richContentPasteTarget === null) { this._richContentPasteTarget = this._findPasteTarget(this._richContentPasteTargetSelector); }
|
||||||
|
return this._richContentPasteTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
_findPasteTarget(selector) {
|
||||||
|
const target = this._document.querySelector(selector);
|
||||||
|
if (target === null) { throw new Error('Clipboard paste target does not exist'); }
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearRichContent(element) {
|
||||||
|
for (const image of element.querySelectorAll('img')) {
|
||||||
|
image.removeAttribute('src');
|
||||||
|
image.removeAttribute('srcset');
|
||||||
|
}
|
||||||
|
element.textContent = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Yomichan",
|
"name": "Yomichan",
|
||||||
"version": "22.9.9.2",
|
"version": "22.10.23.0",
|
||||||
"description": "Japanese dictionary with Anki integration",
|
"description": "Japanese dictionary with Anki integration",
|
||||||
"author": "Alex Yatskov",
|
"author": "Alex Yatskov",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
Loading…
Reference in New Issue
Block a user