Compare commits

..

20 Commits

Author SHA1 Message Date
Gabe
3d7e03ddaf Update version number: 2.0.4 2025-10-26 20:26:52 +08:00
Gabe
1f213bf257 fix: styles 2025-10-26 20:10:13 +08:00
Gabe
b38f079611 fix: change showNotification duration 2025-10-26 19:59:51 +08:00
Gabe
21e639cacd fix: createMutationObserver 2025-10-26 18:42:59 +08:00
Gabe
bdaf665b7c feat: The translation box can be set to adaptive height 2025-10-26 16:18:56 +08:00
Gabe
61a515c1d2 feat: Support multi-touch selection 2025-10-26 00:06:52 +08:00
Gabe
1b646df908 feat: Remember the tranbox position and size 2025-10-25 23:18:39 +08:00
Gabe
5550f939b2 doc: readme 2025-10-25 18:41:20 +08:00
Gabe
b34fb5a600 doc: readme 2025-10-25 18:38:55 +08:00
Gabe
c0dce5c0b1 fix: Optimized text scanning logic 2025-10-25 17:46:29 +08:00
Gabe
d56bd2920f fix: isQualityPoor 2025-10-24 21:44:54 +08:00
Gabe
48ad100a64 fix: Optimized the scan node logic 2025-10-24 21:37:26 +08:00
Gabe
ef07a172a9 doc: custom api 2025-10-24 20:58:08 +08:00
Gabe
f492d47719 fix: disable field of rule 2025-10-24 20:57:10 +08:00
Gabe
ac8c07deb4 fix: keepselector for twitter 2025-10-24 01:46:36 +08:00
Gabe
ca48ab639e fix: remove stopPropagation for shortcut 2025-10-23 19:52:18 +08:00
Gabe
7c5232c1a1 fix: Make keepSelector effective even if richText is disabled 2025-10-23 19:32:59 +08:00
Gabe
4fac7fdfe1 fix: update custom api 2025-10-23 14:35:21 +08:00
Gabe
f7fc9560d5 fix: update custom api 2025-10-23 14:33:12 +08:00
Gabe
f7ba744e7f fix: ignore selector 2025-10-23 11:07:25 +08:00
25 changed files with 251 additions and 101 deletions

2
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=2.0.3
REACT_APP_VERSION=2.0.4
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator

View File

@@ -153,6 +153,10 @@ Custom APIs are very powerful and flexible, and can theoretically connect to any
Example reference: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
### How to directly access the Tampermonkey script settings page
Settings page address: https://fishjar.github.io/kiss-translator/options.html
## Future Plans
This is a side project with no strict timeline. Community contributions are welcome. The following are preliminary feature directions:

View File

@@ -149,6 +149,10 @@
示例参考: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
### 如何直接进入油猴脚本设置页面
设置页面地址: https://fishjar.github.io/kiss-translator/options.html
## 未来规划
本项目为业余开发,无严格时间表,欢迎社区共建。以下为初步设想的功能方向:

View File

@@ -1,5 +1,44 @@
# 自定义接口示例
## 默认接口规范
如果接口的请求数据和返回数据符合以下规范,
则无需填写 `Request Hook``Response Hook`
Request body
```json
{
"texts": ["hello"], // 需要翻译的文本列表
"from":"auto", // 原文语言
"to": "zh-CN" // 目标语言
}
```
Response
```json
[
{
"text": "你好", // 译文
"src": "en" // 原文语言
}
]
```
v2.0.4版后亦支持以下 Response 格式
```json
{
"translations": [ // 译文列表
{
"text": "你好", // 译文
"src": "en" // 原文语言
}
]
}
```
## 谷歌翻译接口
> 此接口不支持聚合

View File

@@ -1,7 +1,7 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "2.0.3",
"version": "2.0.4",
"author": "Gabe<yugang2002@gmail.com>",
"private": true,
"dependencies": {

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.3",
"version": "2.0.4",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.3",
"version": "2.0.4",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.3",
"version": "2.0.4",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -847,7 +847,7 @@ export const parseTransRes = async (
}
return parseAIRes(modelMsg?.content);
case OPT_TRANS_CUSTOMIZE:
return res?.map((item) => [item.text, item.src]);
return (res?.translations ?? res)?.map((item) => [item.text, item.src]);
default:
}

View File

@@ -1160,9 +1160,9 @@ export const I18N = {
zh_TW: `觸控設定`,
},
touch_translate_shortcut: {
zh: `触屏翻译快捷方式`,
en: `Touch Translate Shortcut`,
zh_TW: `觸控翻譯捷徑`,
zh: `触屏翻译快捷方式 (支持多选)`,
en: `Touch Translate Shortcut (multiple supported)`,
zh_TW: `觸控翻譯捷徑 (支援多選)`,
},
touch_tap_0: {
zh: `禁用`,
@@ -1349,6 +1349,11 @@ export const I18N = {
en: `Transbox Follow Selection`,
zh_TW: `翻譯框跟隨選取文字`,
},
tranbox_auto_height: {
zh: `翻译框自适应高度`,
en: `Translation box adaptive height`,
zh_TW: `翻譯框自適應高度`,
},
translate_start_hook: {
zh: `翻译开始钩子函数`,
en: `Translate Start Hook`,

View File

@@ -97,7 +97,7 @@ background: linear-gradient(
export const DEFAULT_SELECTOR =
"h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend";
export const DEFAULT_IGNORE_SELECTOR = "button, footer, pre, mark, nav";
export const DEFAULT_KEEP_SELECTOR = `a:has(code)`;
export const DEFAULT_KEEP_SELECTOR = `code, cite, math, .math, a:has(code)`;
export const DEFAULT_RULE = {
pattern: "", // 匹配网址
selector: "", // 选择器
@@ -211,7 +211,8 @@ const RULES_MAP = {
},
"twitter.com, https://x.com": {
selector: `[data-testid='tweetText']`,
keepSelector: `img, svg, span:has(a), div:has(a)`,
keepSelector: `img, svg, a, span:has(a), div:has(a)`,
ignoreSelector: `button, [data-testid='videoPlayer'], [role='group']`,
autoScan: `false`,
},
"www.youtube.com/live_chat": {

View File

@@ -88,6 +88,7 @@ export const DEFAULT_TRANBOX_SETTING = {
hideClickAway: false, // 是否点击外部关闭弹窗
simpleStyle: false, // 是否简洁界面
followSelection: false, // 翻译框是否跟随选中文本
autoHeight: false, // 自适应高度
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
// extStyles: "", // 附加样式
enDict: OPT_DICT_BING, // 英文词典
@@ -166,7 +167,8 @@ export const DEFAULT_SETTING = {
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
touchTranslate: 2, // 触屏翻译 {5:单指双击6:单指三击7:双指双击}
// touchTranslate: 2, // 触屏翻译 {5:单指双击6:单指三击7:双指双击} (作废)
touchModes: [2], // 触屏翻译 {5:单指双击6:单指三击7:双指双击} (多选)
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
orilist: DEFAULT_ORILIST.join(",\n"), // 禁用CSP名单

View File

@@ -16,6 +16,7 @@ export const STOKEY_RULES = `${APP_NAME}_rules_v${APP_VERSION[0]}`;
export const STOKEY_WORDS = `${APP_NAME}_words`;
export const STOKEY_SYNC = `${APP_NAME}_sync`;
export const STOKEY_FAB = `${APP_NAME}_fab`;
export const STOKEY_TRANBOX = `${APP_NAME}_tranbox`;
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
export const CACHE_NAME = `${APP_NAME}_cache`;

View File

@@ -63,8 +63,8 @@ export const shortcutRegister = (targetKeys = [], fn, target = document) => {
const targetKeySet = new Set(targetKeys);
const onKeyDown = (pressedKeys, event) => {
if (isSameSet(targetKeySet, pressedKeys)) {
// event.preventDefault();
event.stopPropagation();
// event.preventDefault(); // 阻止浏览器的默认行为
// event.stopPropagation(); // 阻止事件继续(向父元素)冒泡
fn();
}
};

View File

@@ -5,6 +5,7 @@ import {
STOKEY_RULES_OLD,
STOKEY_WORDS,
STOKEY_FAB,
STOKEY_TRANBOX,
STOKEY_SYNC,
STOKEY_MSAUTH,
STOKEY_BDAUTH,
@@ -135,6 +136,13 @@ export const getFabWithDefault = async () => (await getFab()) || {};
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
export const putFab = (obj) => putObj(STOKEY_FAB, obj);
/**
* tranbox位置大小
*/
export const getTranBox = () => getObj(STOKEY_TRANBOX);
export const putTranBox = (obj) => putObj(STOKEY_TRANBOX, obj);
export const debouncePutTranBox = debounce(putTranBox, 300);
/**
* 数据同步
*/

View File

@@ -84,7 +84,7 @@ const genStyles = ({
// 虚线框
[OPT_STYLE_DASHBOX]: `
border: 2px dashed ${bgColor || DEFAULT_COLOR};
display: inline-block;
display: block;
padding: 0.2em 0.4em;
box-sizing: border-box;
`,

View File

@@ -77,7 +77,7 @@ export class Translator {
"VIDEO",
]),
INLINE: new Set([
"A",
// "A",
"ABBR",
"ACRONYM",
"B",
@@ -106,7 +106,7 @@ export class Translator {
"SCRIPT",
"SELECT",
"SMALL",
"SPAN",
// "SPAN",
"STRONG",
"SUB",
"SUP",
@@ -206,6 +206,8 @@ export class Translator {
// 14. 包含常见扩展名的文件名 (例如: document.pdf, image.jpeg)
/^[^\s\\/:]+?\.[a-zA-Z0-9]{2,5}$/,
// todo: 数字和特殊字符组成的字符串
];
static DEFAULT_OPTIONS = DEFAULT_SETTING; // 默认配置
@@ -221,6 +223,7 @@ export class Translator {
if (Translator.TAGS.INLINE.has(el.nodeName)) return false;
if (Translator.TAGS.BLOCK.has(el.nodeName)) return true;
if (el.attributes?.display?.value?.includes("inline")) return false;
if (Translator.displayCache.has(el)) {
return Translator.displayCache.get(el);
@@ -231,11 +234,22 @@ export class Translator {
return isBlock;
}
// 判断是否包含块级子元素
static hasBlockNode(el) {
if (!Translator.isElementOrFragment(el)) return false;
for (const child of el.childNodes) {
if (Translator.isBlockNode(child)) {
return true;
}
}
return false;
}
// 判断是否直接包含非空文本节点
static hasTextNode(el) {
if (!Translator.isElementOrFragment(el)) return false;
for (const node of el.childNodes) {
if (node.nodeType === Node.TEXT_NODE && /\S/.test(node.nodeValue)) {
for (const child of el.childNodes) {
if (child.nodeType === Node.TEXT_NODE && /\S/.test(child.nodeValue)) {
return true;
}
}
@@ -248,11 +262,11 @@ export class Translator {
}
// 内置忽略元素
static BUILTIN_IGNORE_SELECTOR = `abbr, address, area, audio, br, canvas, code,
data, datalist, dfn, embed, head, iframe, img, input, kbd, noscript, map,
object, option, output, param, picture, progress,
samp, select, script, style, sub, sup, svg, track, time, textarea, template,
var, video, wbr, .notranslate, [contenteditable], [translate='no'],
static BUILTIN_IGNORE_SELECTOR = `address, area, audio, br, canvas,
data, datalist, embed, head, iframe, input, noscript, map,
object, option, param, picture, progress,
select, script, style, track, textarea, template,
video, wbr, .notranslate, [contenteditable], [translate='no'],
${APP_LCNAME}, #${APP_CONSTS.fabID}, #${APP_CONSTS.boxID},
.${APP_CONSTS.fabID}_warpper, .${APP_CONSTS.boxID}_warpper`;
@@ -528,33 +542,35 @@ export class Translator {
#createMutationObserver() {
return new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (this.#skipMoNodes.has(mutation.target)) return;
if (
mutation.type === "characterData" &&
mutation.oldValue !== mutation.target.nodeValue
this.#skipMoNodes.has(mutation.target) ||
mutation.nextSibling?.tagName === this.#translationTagName
) {
this.#queueForRescan(mutation.target.parentElement);
} else if (mutation.type === "childList") {
if (mutation.nextSibling?.tagName === this.#translationTagName) {
// 恢复原文时插入元素,忽略
continue;
}
continue;
}
if (mutation.type === "characterData") {
if (
mutation.oldValue !== mutation.target.nodeValue &&
!this.#combinedSkipsRegex.test(mutation.target.nodeValue)
) {
this.#queueForRescan(mutation.target.parentElement);
}
} else if (mutation.type === "childList") {
let nodes = new Set();
let hasText = false;
mutation.addedNodes.forEach((node) => {
if (this.#skipMoNodes.has(node)) return;
if (
this.#skipMoNodes.has(node) ||
node.nodeName === this.#translationTagName
) {
return;
}
if (/\S/.test(node.nodeValue)) {
if (node.nodeType === Node.TEXT_NODE) {
hasText = true;
} else if (
Translator.isElementOrFragment(node) &&
node.nodeName !== this.#translationTagName
) {
nodes.add(node);
}
if (node.nodeType === Node.TEXT_NODE) {
hasText = true;
} else if (Translator.isElementOrFragment(node)) {
nodes.add(node);
}
});
if (hasText) {
@@ -772,6 +788,7 @@ export class Translator {
#scanNode(rootNode) {
if (
!Translator.isElementOrFragment(rootNode) ||
// rootNode.matches?.(this.#rule.keepSelector) ||
rootNode.matches?.(this.#ignoreSelector)
) {
return;
@@ -783,13 +800,24 @@ export class Translator {
}
const hasText = Translator.hasTextNode(rootNode);
if (hasText) {
if (!hasText && rootNode.children.length === 1) {
this.#scanNode(rootNode.children[0]);
return;
}
const hasBlock = Translator.hasBlockNode(rootNode);
if (hasText || !hasBlock) {
this.#startObserveNode(rootNode);
}
for (const child of rootNode.children) {
if (!hasText || Translator.isBlockNode(child)) {
this.#scanNode(child);
if (hasBlock) {
for (const child of rootNode.children) {
const isBlock = Translator.isBlockNode(child);
if (!hasText || isBlock) {
this.#scanNode(child);
}
}
}
}
@@ -1028,6 +1056,7 @@ export class Translator {
if (
Translator.TAGS.BREAK_LINE.has(node.nodeName) ||
node.matches?.(this.#ignoreSelector) ||
node.nodeName === this.#translationTagName
) {
return true;
@@ -1240,10 +1269,7 @@ export class Translator {
}
// 文本节点
if (
this.#rule.hasRichText === "false" ||
node.nodeType === Node.TEXT_NODE
) {
if (node.nodeType === Node.TEXT_NODE) {
let text = node.textContent;
// 专业术语替换
@@ -1269,8 +1295,10 @@ export class Translator {
// 元素节点
if (node.nodeType === Node.ELEMENT_NODE) {
if (
Translator.TAGS.REPLACE.has(node.tagName) ||
(this.#rule.hasRichText === "true" &&
Translator.TAGS.REPLACE.has(node.tagName)) ||
node.matches(this.#rule.keepSelector) ||
node.matches(this.#ignoreSelector) ||
!node.textContent.trim()
) {
if (node.tagName === "IMG" || node.tagName === "SVG") {
@@ -1285,7 +1313,10 @@ export class Translator {
innerContent += traverse(child);
});
if (Translator.TAGS.WARP.has(node.tagName)) {
if (
this.#rule.hasRichText === "true" &&
Translator.TAGS.WARP.has(node.tagName)
) {
wrapCounter++;
const startPlaceholder = `<${this.#placeholder.tagName}${wrapCounter}>`;
const endPlaceholder = `</${this.#placeholder.tagName}${wrapCounter}>`;

View File

@@ -28,7 +28,7 @@ import { logger } from "./log";
export default class TranslatorManager {
#clearShortcuts = [];
#menuCommandIds = [];
#clearTouchListener = null;
#clearTouchListeners = [];
#isActive = false;
#isUserscript;
#isIframe;
@@ -110,10 +110,8 @@ export default class TranslatorManager {
this.#clearShortcuts = [];
// 触屏
if (this.#clearTouchListener) {
this.#clearTouchListener();
this.#clearTouchListener = null;
}
this.#clearTouchListeners.forEach((clear) => clear());
this.#clearTouchListeners = [];
// 油猴菜单
if (globalThis.GM && this.#menuCommandIds.length > 0) {
@@ -145,8 +143,8 @@ export default class TranslatorManager {
#setupTouchOperations() {
if (this.#isIframe) return;
const { touchTranslate = 2 } = this._translator.setting;
if (touchTranslate === 0) {
const { touchModes = [2] } = this._translator.setting;
if (touchModes.length === 0) {
return;
}
@@ -154,35 +152,31 @@ export default class TranslatorManager {
this.#processActions({ action: MSG_TRANS_TOGGLE });
};
switch (touchTranslate) {
case 2:
case 3:
case 4:
this.#clearTouchListener = touchTapListener(handleTap, {
taps: 1,
fingers: touchTranslate,
});
break;
case 5:
this.#clearTouchListener = touchTapListener(handleTap, {
taps: 2,
fingers: 1,
});
break;
case 6:
this.#clearTouchListener = touchTapListener(handleTap, {
taps: 3,
fingers: 1,
});
break;
case 7:
this.#clearTouchListener = touchTapListener(handleTap, {
taps: 2,
fingers: 2,
});
break;
default:
}
const handleListener = (mode) => {
let options = null;
switch (mode) {
case 2:
case 3:
case 4:
options = { taps: 1, fingers: mode };
break;
case 5:
options = { taps: 2, fingers: 1 };
break;
case 6:
options = { taps: 3, fingers: 1 };
break;
case 7:
options = { taps: 2, fingers: 2 };
break;
default:
}
if (options) {
this.#clearTouchListeners.push(touchTapListener(handleTap, options));
}
};
touchModes.forEach((mode) => handleListener(mode));
}
#handleWindowMessage(event) {

View File

@@ -590,7 +590,7 @@ class YouTubeCaptionProvider {
return subtitles;
}
#isQualityPoor(lines, lengthThreshold = 250, percentageThreshold = 0.1) {
#isQualityPoor(lines, lengthThreshold = 250, percentageThreshold = 0.2) {
if (lines.length === 0) return false;
const longLinesCount = lines.filter(
(line) => line.text.length > lengthThreshold
@@ -913,7 +913,7 @@ class YouTubeCaptionProvider {
}
}
#showNotification(message, duration = 3000) {
#showNotification(message, duration = 2000) {
if (!this.#notificationEl) this.#createNotificationElement();
this.#notificationEl.textContent = message;
this.#notificationEl.style.opacity = "1";

View File

@@ -459,6 +459,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
type="number"
name="splitLength"
value={splitLength}
disabled={disabled}
onChange={handleChange}
min={0}
max={1000}

View File

@@ -94,7 +94,7 @@ export default function Settings() {
newlineLength = TRANS_NEWLINE_LENGTH,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
contextMenuType = 1,
touchTranslate = 2,
touchModes = [2],
blacklist = DEFAULT_BLACKLIST.join(",\n"),
csplist = DEFAULT_CSPLIST.join(",\n"),
orilist = DEFAULT_ORILIST.join(",\n"),
@@ -268,10 +268,13 @@ export default function Settings() {
select
fullWidth
size="small"
name="touchTranslate"
value={touchTranslate}
name="touchModes"
value={touchModes}
label={i18n("touch_translate_shortcut")}
onChange={handleChange}
SelectProps={{
multiple: true,
}}
>
{[0, 2, 3, 4, 5, 6, 7].map((item) => (
<MenuItem key={item} value={item}>

View File

@@ -68,6 +68,7 @@ export default function Tranbox() {
hideClickAway = false,
simpleStyle = false,
followSelection = false,
autoHeight = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
// extStyles = "",
enDict = OPT_DICT_BING,
@@ -330,6 +331,20 @@ export default function Tranbox() {
max={200}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
fullWidth
select
size="small"
name="autoHeight"
value={autoHeight}
label={i18n("tranbox_auto_height")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
{!isExt && (
<Grid item xs={12} sm={12} md={6} lg={3}>
<ShortcutInput

View File

@@ -150,6 +150,7 @@ export default function DraggableResizable({
setPosition,
onChangeSize,
onChangePosition,
autoHeight,
...props
}) {
const lineWidth = 4;
@@ -222,11 +223,19 @@ export default function DraggableResizable({
</Pointer>
<Box
className="KT-draggable-container"
style={{
width: size.w,
height: size.h,
overflow: "hidden auto",
}}
style={
autoHeight
? {
width: size.w,
maxHeight: size.h,
overflow: "hidden auto",
}
: {
width: size.w,
height: size.h,
overflow: "hidden auto",
}
}
>
{children}
</Box>

View File

@@ -115,7 +115,15 @@ export default function TranBox({
text,
setText,
setShowBox,
tranboxSetting: { enDict, enSug, apiSlugs, fromLang, toLang, toLang2 },
tranboxSetting: {
enDict,
enSug,
apiSlugs,
fromLang,
toLang,
toLang2,
autoHeight,
},
transApis,
boxSize,
setBoxSize,
@@ -141,6 +149,7 @@ export default function TranBox({
size={boxSize}
setSize={setBoxSize}
setPosition={setBoxPosition}
autoHeight={autoHeight}
header={
<Header
setShowBox={setShowBox}

View File

@@ -15,6 +15,7 @@ import {
import { isMobile } from "../../libs/mobile";
import { kissLog } from "../../libs/log";
import { useLangMap } from "../../hooks/I18n";
import { debouncePutTranBox, getTranBox } from "../../libs/storage";
export default function Slection({
contextMenuType,
@@ -107,6 +108,29 @@ export default function Slection({
return "onMouseUp";
}, [triggerMode]);
useEffect(() => {
(async () => {
try {
const { w, h, x, y } = (await getTranBox()) || {};
if (w !== undefined && h !== undefined) {
setBoxSize({ w, h });
}
if (x !== undefined && y !== undefined) {
setBoxPosition({
x: limitNumber(x, 0, window.innerWidth),
y: limitNumber(y, 0, window.innerHeight),
});
}
} catch (err) {
//
}
})();
}, []);
useEffect(() => {
debouncePutTranBox({ ...boxSize, ...boxPosition });
}, [boxSize, boxPosition]);
useEffect(() => {
async function handleMouseup(e) {
e.stopPropagation();