From 534eaed1ed5998cfb36ca5d6db414c2e44be5f09 Mon Sep 17 00:00:00 2001 From: Gabe Yuan Date: Mon, 18 Dec 2023 11:46:37 +0800 Subject: [PATCH] refactor: input translate --- src/common.js | 4 + src/libs/inputTranslate.js | 205 +++++++++++++++++++++++++++++++++++ src/libs/translator.js | 211 +------------------------------------ 3 files changed, 210 insertions(+), 210 deletions(-) create mode 100644 src/libs/inputTranslate.js diff --git a/src/common.js b/src/common.js index e74e772..eec6e83 100644 --- a/src/common.js +++ b/src/common.js @@ -24,6 +24,7 @@ import { runWebfix } from "./libs/webfix"; import { matchRule } from "./libs/rules"; import { trySyncAllSubRules } from "./libs/subRules"; import { isInBlacklist } from "./libs/blacklist"; +import inputTranslate from "./libs/inputTranslate"; /** * 油猴脚本设置页面 @@ -265,6 +266,9 @@ export async function run(isUserscript = false) { windowListener(rule); !isUserscript && runtimeListener(translator); + // 输入框翻译 + inputTranslate(setting); + // 划词翻译 showTransbox(setting); diff --git a/src/libs/inputTranslate.js b/src/libs/inputTranslate.js new file mode 100644 index 0000000..aaef6f7 --- /dev/null +++ b/src/libs/inputTranslate.js @@ -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 + ); +} diff --git a/src/libs/translator.js b/src/libs/translator.js index 9149f7c..37c5055 100644 --- a/src/libs/translator.js +++ b/src/libs/translator.js @@ -9,101 +9,16 @@ import { SHADOW_KEY, OPT_MOUSEKEY_DISABLE, OPT_MOUSEKEY_MOUSEOVER, - DEFAULT_INPUT_RULE, - DEFAULT_TRANS_APIS, - DEFAULT_INPUT_SHORTCUT, - OPT_LANGS_LIST, } from "../config"; import Content from "../views/Content"; import { updateFetchPool, clearFetchPool } from "./fetch"; -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(node, loadingId) { - const div = node.offsetParent.querySelector(`#${loadingId}`); - if (div) { - div.remove(); - } -} +import { debounce, genEventName } from "./utils"; /** * 翻译类 */ export class Translator { _rule = {}; - _inputRule = {}; _setting = {}; _rootNodes = new Set(); _tranNodes = new Map(); @@ -187,11 +102,6 @@ export class Translator { if (rule.transOpen === "true") { this._register(); } - - this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE; - if (this._inputRule.transOpen) { - this._registerInput(); - } } 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) => { // console.log("mouseenter", e); if (!this._tranNodes.has(e.target)) {