input box trans

This commit is contained in:
Gabe Yuan
2023-09-15 17:25:58 +08:00
parent e7a57ad3b2
commit 938c123412
5 changed files with 153 additions and 95 deletions

View File

@@ -190,7 +190,6 @@ export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
export const GLOBLA_RULE = {
pattern: "*",
selector: DEFAULT_SELECTOR,
inputSelector: "",
translator: OPT_TRANS_MICROSOFT,
fromLang: "auto",
toLang: "zh-CN",

View File

@@ -10,7 +10,6 @@ export const SHADOW_KEY = ">>>";
export const DEFAULT_RULE = {
pattern: "",
selector: "",
inputSelector: "",
translator: GLOBAL_KEY,
fromLang: GLOBAL_KEY,
toLang: GLOBAL_KEY,

View File

@@ -1,5 +1,5 @@
export const loadingSvg = `
<svg viewBox="0 0 100 100" style="display:block; width:100%; height: 100%;">
<svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%;">
<circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6">
<animateTransform
attributeName="transform"

View File

@@ -16,10 +16,87 @@ import {
} from "../config";
import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce, genEventName, removeEndchar, matchInputStr } from "./utils";
import {
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(loadingId) {
const div = document.getElementById(loadingId);
if (div) {
div.remove();
}
}
/**
* 翻译类
@@ -46,7 +123,6 @@ export class Translator {
"script",
"iframe",
];
_inputNodeNames = ["INPUT", "TEXTAREA"];
_eventName = genEventName();
// 显示
@@ -111,9 +187,8 @@ export class Translator {
this._register();
}
const inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
this._inputRule = { ...inputRule, selector: rule.inputSelector };
if (inputRule.transOpen && rule.inputSelector) {
this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
if (setting.inputRule.transOpen) {
this._registerInput();
}
}
@@ -265,7 +340,6 @@ export class Translator {
fromLang,
toLang,
triggerCount,
selector,
transSign,
} = this._inputRule;
const apiSetting = (this._setting.transApis || DEFAULT_TRANS_APIS)[
@@ -279,90 +353,88 @@ export class Translator {
stepShortcutRegister(
triggerShortcut,
() => {
document.querySelectorAll(selector).forEach(async (node) => {
let text = "";
let num = 0;
let timer;
async () => {
const node = document.activeElement;
if (!node || !(isInputNode(node) || isEditAbleNode(node))) {
return;
}
if (this._inputNodeNames.includes(node.nodeName)) {
text = node.value || "";
} else {
text = node.textContent || "";
let text = getNodeText(node);
const loadingId = "kiss-" + genEventName();
// todo: remove multiple char
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
text = removeEndchar(text, triggerShortcut[0], triggerCount);
}
if (!text.trim()) {
return;
}
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];
}
}
// todo: remove multiple char
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
text = removeEndchar(text, triggerShortcut[0], triggerCount);
}
// console.log("input -->", text);
if (!text.trim()) {
try {
addLoading(node, loadingId);
await sleep(2000);
const deLang = await tryDetectLang(text);
if (deLang && toLang.includes(deLang)) {
return;
}
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];
}
const [trText, isSame] = await apiTranslate({
translator,
text,
fromLang,
toLang,
apiSetting,
});
if (!trText || isSame) {
return;
}
// console.log("input -->", text);
try {
const deLang = await tryDetectLang(text);
if (deLang && toLang.includes(deLang)) {
return;
}
timer = setInterval(() => {
const loadingText = `${text} ${"-\\|/"[++num % 4]} `;
if (this._inputNodeNames.includes(node.nodeName)) {
node.value = loadingText;
} else {
node.textContent = loadingText;
}
}, 200);
const [trText, isSame] = await apiTranslate({
translator,
text,
fromLang,
toLang,
apiSetting,
});
if (!trText || isSame) {
throw new Error("same lang or no res");
}
clearInterval(timer);
if (this._inputNodeNames.includes(node.nodeName)) {
node.value = trText;
node.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true })
);
} else {
node.textContent = trText;
}
} catch (err) {
console.log("[translate input]", err.message);
timer && clearInterval(timer);
if (this._inputNodeNames.includes(node.nodeName)) {
node.value = text;
} else {
node.textContent = text;
}
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);
if (getNodeText(node).startsWith(text)) {
pasteContentCommand(node, trText);
await sleep(100);
} else {
collapseToEnd(node);
}
} catch (err) {
console.log("[translate input]", err.message);
} finally {
removeLoading(loadingId);
}
},
triggerCount
);

View File

@@ -64,7 +64,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
const {
pattern,
selector,
inputSelector = "",
translator,
fromLang,
toLang,
@@ -179,17 +178,6 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
onFocus={handleFocus}
multiline
/>
<TextField
size="small"
label={i18n("input_selector")}
helperText={i18n("input_selector_helper")}
name="inputSelector"
value={inputSelector}
disabled={disabled}
onChange={handleChange}
onFocus={handleFocus}
multiline
/>
<Box>
<Grid container spacing={2} columns={12}>