refactor: input translate
This commit is contained in:
@@ -24,6 +24,7 @@ import { runWebfix } from "./libs/webfix";
|
|||||||
import { matchRule } from "./libs/rules";
|
import { matchRule } from "./libs/rules";
|
||||||
import { trySyncAllSubRules } from "./libs/subRules";
|
import { trySyncAllSubRules } from "./libs/subRules";
|
||||||
import { isInBlacklist } from "./libs/blacklist";
|
import { isInBlacklist } from "./libs/blacklist";
|
||||||
|
import inputTranslate from "./libs/inputTranslate";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 油猴脚本设置页面
|
* 油猴脚本设置页面
|
||||||
@@ -265,6 +266,9 @@ export async function run(isUserscript = false) {
|
|||||||
windowListener(rule);
|
windowListener(rule);
|
||||||
!isUserscript && runtimeListener(translator);
|
!isUserscript && runtimeListener(translator);
|
||||||
|
|
||||||
|
// 输入框翻译
|
||||||
|
inputTranslate(setting);
|
||||||
|
|
||||||
// 划词翻译
|
// 划词翻译
|
||||||
showTransbox(setting);
|
showTransbox(setting);
|
||||||
|
|
||||||
|
|||||||
205
src/libs/inputTranslate.js
Normal file
205
src/libs/inputTranslate.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_INPUT_RULE,
|
||||||
|
DEFAULT_TRANS_APIS,
|
||||||
|
DEFAULT_INPUT_SHORTCUT,
|
||||||
|
OPT_LANGS_LIST,
|
||||||
|
} from "../config";
|
||||||
|
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
|
||||||
|
import { stepShortcutRegister } from "./shortcut";
|
||||||
|
import { apiTranslate } from "../apis";
|
||||||
|
import { tryDetectLang } from ".";
|
||||||
|
import { loadingSvg } from "./svg";
|
||||||
|
|
||||||
|
function isInputNode(node) {
|
||||||
|
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEditAbleNode(node) {
|
||||||
|
return node.hasAttribute("contenteditable");
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectContent(node) {
|
||||||
|
node.focus();
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(node);
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteContentEvent(node, text) {
|
||||||
|
node.focus();
|
||||||
|
const data = new DataTransfer();
|
||||||
|
data.setData("text/plain", text);
|
||||||
|
|
||||||
|
const event = new ClipboardEvent("paste", { clipboardData: data });
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
data.clearData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteContentCommand(node, text) {
|
||||||
|
node.focus();
|
||||||
|
document.execCommand("insertText", false, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapseToEnd(node) {
|
||||||
|
node.focus();
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.collapseToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeText(node) {
|
||||||
|
if (isInputNode(node)) {
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
return node.innerText || node.textContent || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLoading(node, loadingId) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.id = loadingId;
|
||||||
|
div.innerHTML = loadingSvg;
|
||||||
|
div.style.cssText = `
|
||||||
|
width: ${node.offsetWidth}px;
|
||||||
|
height: ${node.offsetHeight}px;
|
||||||
|
line-height: ${node.offsetHeight}px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
left: ${node.offsetLeft}px;
|
||||||
|
top: ${node.offsetTop}px;
|
||||||
|
z-index: 2147483647;
|
||||||
|
`;
|
||||||
|
node.offsetParent?.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLoading(node, loadingId) {
|
||||||
|
const div = node.offsetParent.querySelector(`#${loadingId}`);
|
||||||
|
if (div) {
|
||||||
|
div.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入框翻译
|
||||||
|
*/
|
||||||
|
export default function inputTranslate ({
|
||||||
|
inputRule: {
|
||||||
|
transOpen,
|
||||||
|
triggerShortcut,
|
||||||
|
translator,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
triggerCount,
|
||||||
|
triggerTime,
|
||||||
|
transSign,
|
||||||
|
} = DEFAULT_INPUT_RULE,
|
||||||
|
transApis,
|
||||||
|
detectRemote,
|
||||||
|
}) {
|
||||||
|
if (!transOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiSetting = transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
|
||||||
|
if (triggerShortcut.length === 0) {
|
||||||
|
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||||
|
triggerCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
stepShortcutRegister(
|
||||||
|
triggerShortcut,
|
||||||
|
async () => {
|
||||||
|
let node = document.activeElement;
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (node.shadowRoot) {
|
||||||
|
node = node.shadowRoot.activeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInputNode(node) && !isEditAbleNode(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let initText = getNodeText(node);
|
||||||
|
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
|
||||||
|
// todo: remove multiple char
|
||||||
|
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
|
||||||
|
}
|
||||||
|
if (!initText.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = initText;
|
||||||
|
if (transSign) {
|
||||||
|
const res = matchInputStr(text, transSign);
|
||||||
|
if (res) {
|
||||||
|
let lang = res[1];
|
||||||
|
if (lang === "zh" || lang === "cn") {
|
||||||
|
lang = "zh-CN";
|
||||||
|
} else if (lang === "tw" || lang === "hk") {
|
||||||
|
lang = "zh-TW";
|
||||||
|
}
|
||||||
|
if (lang && OPT_LANGS_LIST.includes(lang)) {
|
||||||
|
toLang = lang;
|
||||||
|
}
|
||||||
|
text = res[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("input -->", text);
|
||||||
|
|
||||||
|
const loadingId = "kiss-" + genEventName();
|
||||||
|
try {
|
||||||
|
addLoading(node, loadingId);
|
||||||
|
|
||||||
|
const deLang = await tryDetectLang(text, detectRemote);
|
||||||
|
if (deLang && toLang.includes(deLang)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [trText, isSame] = await apiTranslate({
|
||||||
|
translator,
|
||||||
|
text,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
apiSetting,
|
||||||
|
});
|
||||||
|
if (!trText || isSame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInputNode(node)) {
|
||||||
|
node.value = trText;
|
||||||
|
node.dispatchEvent(
|
||||||
|
new Event("input", { bubbles: true, cancelable: true })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectContent(node);
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
pasteContentEvent(node, trText);
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
// todo: use includes?
|
||||||
|
if (getNodeText(node).startsWith(initText)) {
|
||||||
|
pasteContentCommand(node, trText);
|
||||||
|
await sleep(100);
|
||||||
|
} else {
|
||||||
|
collapseToEnd(node);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[translate input]", err.message);
|
||||||
|
} finally {
|
||||||
|
removeLoading(node, loadingId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
triggerCount,
|
||||||
|
triggerTime
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,101 +9,16 @@ import {
|
|||||||
SHADOW_KEY,
|
SHADOW_KEY,
|
||||||
OPT_MOUSEKEY_DISABLE,
|
OPT_MOUSEKEY_DISABLE,
|
||||||
OPT_MOUSEKEY_MOUSEOVER,
|
OPT_MOUSEKEY_MOUSEOVER,
|
||||||
DEFAULT_INPUT_RULE,
|
|
||||||
DEFAULT_TRANS_APIS,
|
|
||||||
DEFAULT_INPUT_SHORTCUT,
|
|
||||||
OPT_LANGS_LIST,
|
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import Content from "../views/Content";
|
import Content from "../views/Content";
|
||||||
import { updateFetchPool, clearFetchPool } from "./fetch";
|
import { updateFetchPool, clearFetchPool } from "./fetch";
|
||||||
import {
|
import { debounce, genEventName } from "./utils";
|
||||||
debounce,
|
|
||||||
genEventName,
|
|
||||||
removeEndchar,
|
|
||||||
matchInputStr,
|
|
||||||
sleep,
|
|
||||||
} from "./utils";
|
|
||||||
import { stepShortcutRegister } from "./shortcut";
|
|
||||||
import { apiTranslate } from "../apis";
|
|
||||||
import { tryDetectLang } from ".";
|
|
||||||
import { loadingSvg } from "./svg";
|
|
||||||
|
|
||||||
function isInputNode(node) {
|
|
||||||
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEditAbleNode(node) {
|
|
||||||
return node.hasAttribute("contenteditable");
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectContent(node) {
|
|
||||||
node.focus();
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(node);
|
|
||||||
|
|
||||||
const selection = window.getSelection();
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pasteContentEvent(node, text) {
|
|
||||||
node.focus();
|
|
||||||
const data = new DataTransfer();
|
|
||||||
data.setData("text/plain", text);
|
|
||||||
|
|
||||||
const event = new ClipboardEvent("paste", { clipboardData: data });
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
data.clearData();
|
|
||||||
}
|
|
||||||
|
|
||||||
function pasteContentCommand(node, text) {
|
|
||||||
node.focus();
|
|
||||||
document.execCommand("insertText", false, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function collapseToEnd(node) {
|
|
||||||
node.focus();
|
|
||||||
const selection = window.getSelection();
|
|
||||||
selection.collapseToEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNodeText(node) {
|
|
||||||
if (isInputNode(node)) {
|
|
||||||
return node.value;
|
|
||||||
}
|
|
||||||
return node.innerText || node.textContent || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLoading(node, loadingId) {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.id = loadingId;
|
|
||||||
div.innerHTML = loadingSvg;
|
|
||||||
div.style.cssText = `
|
|
||||||
width: ${node.offsetWidth}px;
|
|
||||||
height: ${node.offsetHeight}px;
|
|
||||||
line-height: ${node.offsetHeight}px;
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
left: ${node.offsetLeft}px;
|
|
||||||
top: ${node.offsetTop}px;
|
|
||||||
z-index: 2147483647;
|
|
||||||
`;
|
|
||||||
node.offsetParent?.appendChild(div);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeLoading(node, loadingId) {
|
|
||||||
const div = node.offsetParent.querySelector(`#${loadingId}`);
|
|
||||||
if (div) {
|
|
||||||
div.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 翻译类
|
* 翻译类
|
||||||
*/
|
*/
|
||||||
export class Translator {
|
export class Translator {
|
||||||
_rule = {};
|
_rule = {};
|
||||||
_inputRule = {};
|
|
||||||
_setting = {};
|
_setting = {};
|
||||||
_rootNodes = new Set();
|
_rootNodes = new Set();
|
||||||
_tranNodes = new Map();
|
_tranNodes = new Map();
|
||||||
@@ -187,11 +102,6 @@ export class Translator {
|
|||||||
if (rule.transOpen === "true") {
|
if (rule.transOpen === "true") {
|
||||||
this._register();
|
this._register();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
|
|
||||||
if (this._inputRule.transOpen) {
|
|
||||||
this._registerInput();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get setting() {
|
get setting() {
|
||||||
@@ -355,125 +265,6 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_registerInput = () => {
|
|
||||||
const {
|
|
||||||
triggerShortcut: initTriggerShortcut,
|
|
||||||
translator,
|
|
||||||
fromLang,
|
|
||||||
toLang: initToLang,
|
|
||||||
triggerCount: initTriggerCount,
|
|
||||||
triggerTime,
|
|
||||||
transSign,
|
|
||||||
} = this._inputRule;
|
|
||||||
const apiSetting =
|
|
||||||
this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
|
|
||||||
const { detectRemote } = this._setting;
|
|
||||||
|
|
||||||
let triggerShortcut = initTriggerShortcut;
|
|
||||||
let triggerCount = initTriggerCount;
|
|
||||||
if (triggerShortcut.length === 0) {
|
|
||||||
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
|
||||||
triggerCount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
stepShortcutRegister(
|
|
||||||
triggerShortcut,
|
|
||||||
async () => {
|
|
||||||
let node = document.activeElement;
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (node.shadowRoot) {
|
|
||||||
node = node.shadowRoot.activeElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInputNode(node) && !isEditAbleNode(node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let initText = getNodeText(node);
|
|
||||||
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
|
|
||||||
// todo: remove multiple char
|
|
||||||
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
|
|
||||||
}
|
|
||||||
if (!initText.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = initText;
|
|
||||||
let toLang = initToLang;
|
|
||||||
if (transSign) {
|
|
||||||
const res = matchInputStr(text, transSign);
|
|
||||||
if (res) {
|
|
||||||
let lang = res[1];
|
|
||||||
if (lang === "zh" || lang === "cn") {
|
|
||||||
lang = "zh-CN";
|
|
||||||
} else if (lang === "tw" || lang === "hk") {
|
|
||||||
lang = "zh-TW";
|
|
||||||
}
|
|
||||||
if (lang && OPT_LANGS_LIST.includes(lang)) {
|
|
||||||
toLang = lang;
|
|
||||||
}
|
|
||||||
text = res[2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log("input -->", text);
|
|
||||||
|
|
||||||
const loadingId = "kiss-" + genEventName();
|
|
||||||
try {
|
|
||||||
addLoading(node, loadingId);
|
|
||||||
|
|
||||||
const deLang = await tryDetectLang(text, detectRemote);
|
|
||||||
if (deLang && toLang.includes(deLang)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [trText, isSame] = await apiTranslate({
|
|
||||||
translator,
|
|
||||||
text,
|
|
||||||
fromLang,
|
|
||||||
toLang,
|
|
||||||
apiSetting,
|
|
||||||
});
|
|
||||||
if (!trText || isSame) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInputNode(node)) {
|
|
||||||
node.value = trText;
|
|
||||||
node.dispatchEvent(
|
|
||||||
new Event("input", { bubbles: true, cancelable: true })
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectContent(node);
|
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
pasteContentEvent(node, trText);
|
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// todo: use includes?
|
|
||||||
if (getNodeText(node).startsWith(initText)) {
|
|
||||||
pasteContentCommand(node, trText);
|
|
||||||
await sleep(100);
|
|
||||||
} else {
|
|
||||||
collapseToEnd(node);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[translate input]", err.message);
|
|
||||||
} finally {
|
|
||||||
removeLoading(node, loadingId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
triggerCount,
|
|
||||||
triggerTime
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
_handleMouseover = (e) => {
|
_handleMouseover = (e) => {
|
||||||
// console.log("mouseenter", e);
|
// console.log("mouseenter", e);
|
||||||
if (!this._tranNodes.has(e.target)) {
|
if (!this._tranNodes.has(e.target)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user