Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.greasyfork.org/scripts/534402/1580100/frame.js
// trimmed from https://github.com/barrer/tampermonkey-script/blob/master/translate/translate-dictionary.js
; const {
PushContextMenu,
PushIconAction,
PushInitialFn,
initContextMenu,
initIconActions,
request,
parseKey,
tapKeyboard,
readClipboard,
requestEx,
getSelectionElement,
buildOption,
htmlSpecial,
decodeHtmlSpecial,
base64ToUint8Array,
} = (() => {
const contextMenuActions = [];
const iconActions = [];
const helperServerHost = GM_getValue('host', 'http://127.0.0.1:9999');
const initialFns = [];
function PushInitialFn(...fn) {
initialFns.push(...fn);
}
function PushContextMenu(...fn) {
contextMenuActions.push(...fn);
}
function PushIconAction(...fn) {
iconActions.push(...fn);
}
function initContextMenu() {
contextMenuActions.forEach(menu => {
let fn = menu.action;
if (typeof menu.action === 'string') {
fn = () => {
request('keys=' + parseKey(menu.action), menu.path, menu.hasOwnProperty('call') ? menu.call : null)
}
} else if (typeof menu.action === 'object') {
fn = () => {
request(menu.action, menu.path, menu.hasOwnProperty('call') ? menu.call : null)
}
}
GM_registerMenuCommand(menu.title, () => {
if (self === top) {
fn();
}
}, menu.key);
})
}
function initIconActions() {
String.prototype.replaceWithMap = function (m) {
let s = this;
Object.keys(k => {
s = s.replaceAll(k, m[k]);
})
return s
}
/**样式*/
const style = document.createElement('style');
const fontSize = 14; // 字体大小
const iconWidth = 300; // 整个面板宽度
const iconHeight = 400; // 整个面板高度
// 可以自定义的变量 <<<<< (自定义变量修改后把 “@version” 版本号改为 “10000” 防止更新后消失)
const trContentWidth = iconWidth - 16; // 整个面板宽度 - 边距间隔 = 翻译正文宽度
const trContentHeight = iconHeight - 35; // 整个面板高度 - 边距间隔 = 翻译正文高度
const zIndex = '2147483647'; // 渲染图层
style.textContent = GM_getResourceText('style').replaceWithMap({
'${fontSize}': fontSize,
'${zIndex}': zIndex,
'${trContentWidth}': trContentWidth,
'${trContentHeight}': trContentHeight,
});
// iframe 工具库
const iframe = document.createElement('iframe');
let iframeWin = null;
iframe.style.display = 'none';
let icon = document.createElement('tr-icon'); //翻译图标
let content = document.createElement('tr-content'), // 内容面板
contentList = document.createElement('div'), //翻译内容结果集(HTML内容)列表
selected, // 当前选中文本
pageX, // 图标显示的 X 坐标
pageY; // 图标显示的 Y 坐标
// 初始化内容面板
content.appendChild(contentList);
// 绑定图标拖动事件
const iconDrag = new Drag(icon);
// 图标数组
let hideCalls = []
// 添加翻译引擎图标
iconActions.forEach(obj => {
const img = document.createElement('img');
img.setAttribute('src', obj.image);
img.setAttribute('alt', obj.name);
img.setAttribute('title', obj.name);
img.setAttribute('icon-id', obj.id);
if (obj.hasOwnProperty('trigger') && obj.trigger) {
img.addEventListener('click', (event) => {
obj.trigger(selected, hideIcon, event);
});
}
icon.appendChild(img);
if (obj.hide) {
hideCalls.push(obj.hide)
}
if (obj.hasOwnProperty('call') && obj.call) {
obj.call(img, content);
}
});
// 添加内容面板(放图标后面)
icon.appendChild(content);
// 添加样式、翻译图标到 DOM
const root = document.createElement('div');
document.documentElement.appendChild(root);
const shadow = root.attachShadow({
mode: 'closed'
});
// iframe 工具库加入 Shadow
shadow.appendChild(iframe);
iframeWin = iframe.contentWindow;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = createObjectURLWithTry(new Blob(['\ufeff', style.textContent], {
type: 'text/css;charset=UTF-8'
}));
// 多种方式最大化兼容:Content Security Policy
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
shadow.appendChild(style); // 内部样式表
shadow.appendChild(link); // 外部样式表
// 翻译图标加入 Shadow
shadow.appendChild(icon);
initialFns.length > 0 && initialFns.forEach(fn => fn(shadow));
// 鼠标事件:防止选中的文本消失
document.addEventListener('mousedown', function (e) {
log('mousedown event:', e);
if (e.target === icon || (e.target.parentNode && e.target.parentNode === icon)) { // 点击了翻译图标
e.preventDefault();
}
});
// 鼠标事件:防止选中的文本消失;显示、隐藏翻译图标
document.addEventListener('mouseup', showIcon);
// 选中变化事件
document.addEventListener('selectionchange', showIcon);
document.addEventListener('touchend', showIcon);
/**日志输出*/
function log() {
const debug = false;
if (!debug) {
return;
}
if (arguments) {
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
}
function isInShadow(ele) {
if (ele === root) {
return true
}
if (!ele || !ele.hasOwnProperty('parentElement') || !ele.parentElement.TagName) {
return false
}
const tag = ele.parentElement.TagName;
if (tag === "TR-CONTENT") {
return true
}
if (!tag) {
return false
}
isInShadow(ele.parentElement);
}
/**鼠标拖动*/
function Drag(element) {
this.dragging = false;
this.mouseDownPositionX = 0;
this.mouseDownPositionY = 0;
this.elementOriginalLeft = parseInt(element.style.left);
this.elementOriginalTop = parseInt(element.style.top);
const ref = this;
this.startDrag = function (e) {
if (e.target !== element) {
return
}
e.preventDefault();
ref.dragging = true;
ref.startDragTime = new Date().getTime();
ref.mouseDownPositionX = e.clientX;
ref.mouseDownPositionY = e.clientY;
ref.elementOriginalLeft = parseInt(element.style.left);
ref.elementOriginalTop = parseInt(element.style.top);
// set mousemove event
window.addEventListener('mousemove', ref.dragElement);
log('startDrag');
};
this.unsetMouseMove = function () {
// unset mousemove event
window.removeEventListener('mousemove', ref.dragElement);
};
this.stopDrag = function (e) {
e.preventDefault();
ref.dragging = false;
ref.stopDragTime = new Date().getTime();
ref.unsetMouseMove();
log('stopDrag');
};
this.dragElement = function (e) {
log('dragging');
if (!ref.dragging) {
return;
}
e.preventDefault();
// move element
element.style.left = ref.elementOriginalLeft + (e.clientX - ref.mouseDownPositionX) + 'px';
element.style.top = ref.elementOriginalTop + (e.clientY - ref.mouseDownPositionY) + 'px';
log('dragElement');
};
element.onmousedown = this.startDrag;
element.onmouseup = this.stopDrag;
}
/**强制结束拖动*/
function forceStopDrag() {
if (iconDrag) {
// 强制设置鼠标拖动事件结束,防止由于网页本身的其它鼠标事件冲突而导致没有侦测到:mouseup
iconDrag.dragging = false;
iconDrag.unsetMouseMove();
}
}
// html 字符串转 DOM
/**带异常处理的 createObjectURL*/
function createObjectURLWithTry(blob) {
try {
return iframeWin.URL.createObjectURL(blob);
} catch (error) {
log(error);
}
return '';
}
/**显示 icon*/
function showIcon(e) {
log('showIcon event:', e);
let offsetX = 4; // 横坐标翻译图标偏移
let offsetY = 8; // 纵坐标翻译图标偏移
// 更新翻译图标 X、Y 坐标
if (e.pageX && e.pageY) { // 鼠标
log('mouse pageX/Y');
pageX = e.pageX;
pageY = e.pageY;
}
if (e.changedTouches) { // 触屏
if (e.changedTouches.length > 0) { // 多点触控选取第 1 个
log('touch pageX/Y');
pageX = e.changedTouches[0].pageX;
pageY = e.changedTouches[0].pageY;
// 触屏修改翻译图标偏移(Android、iOS 选中后的动作菜单一般在当前文字顶部,翻译图标则放到底部)
offsetX = -26; // 单个翻译图标块宽度
offsetY = 16 * 3; // 一般字体高度的 3 倍,距离系统自带动作菜单、选择光标太近会导致无法点按
}
}
log('selected:' + selected + ', pageX:' + pageX + ', pageY:' + pageY)
if (e.target === icon || (e.target.parentNode && e.target.parentNode === icon)) { // 点击了翻译图标
e.preventDefault();
return;
}
selected = window.getSelection().toString().trim(); // 当前选中文本
log('selected:' + selected + ', icon display:' + icon.style.display);
if (selected && icon.style.display !== 'block' && pageX && pageY) { // 显示翻译图标
log('show icon');
icon.style.top = pageY + offsetY + 'px';
icon.style.left = pageX + offsetX + 'px';
icon.style.display = 'block';
// 兼容部分 Content Security Policy
icon.style.position = 'absolute';
icon.style.zIndex = zIndex;
} else if (!selected && e.target !== document && !isInShadow(e.target)) { // 隐藏翻译图标
log('hide icon');
hideIcon();
}
}
/**隐藏 icon*/
function hideIcon() {
icon.style.display = 'none';
content.style.display = 'none';
pageX = 0;
pageY = 0;
forceStopDrag();
if (hideCalls.length > 0) {
hideCalls.forEach(fn => {
fn(icon)
})
}
}
}
async function request(data, path = '', call = null) {
data = data ? buildData(data, path) : '';
if (path !== '' && path[0] !== '/') {
path = '/' + path;
}
await GM_xmlhttpRequest({
method: "POST",
url: helperServerHost + path,
data: data,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
onload: function (res) {
if (call) {
call(res);
}
},
onerror: function (res) {
console.log(res);
},
onabort: function (res) {
console.log(res);
}
});
}
function parseKey(key) {
key = key.trim()
if (key.indexOf('[') > -1) {
return key
}
const keys = key.split(',').map(v => {
v = v.trim()
const vv = v.split(' ')
if (vv.length > 1) {
const k = vv[vv.length - 1]
let kk = vv.slice(0, vv.length - 1)
kk.unshift(k)
return kk
}
return vv
})
return JSON.stringify(keys)
}
function buildData(data) {
if (typeof data === 'object') {
data = Object.keys(data).map(k => {
if (data[k] instanceof Array) {
return data[k].map(v => k + '=' + encodeURIComponent(v)).join('&')
}
return k + '=' + encodeURIComponent(data[k])
}).join('&');
}
return data
}
async function tapKeyboard(keys) {
await request('keys=' + parseKey(keys))
}
async function readClipboard(type = 0) {
const { responseText: text } = await requestEx(helperServerHost + '/clipboard?type=' + (type === 1 ? 'img' : 'text'));
return text
}
async function requestEx(url, data = '', options = {}) {
data = buildData(data)
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url: url,
data: data,
method: 'GET',
onload: function (res) {
return resolve(res)
},
onerror: function (res) {
reject(res)
},
...options
});
})
}
function getSelectionElement() {
const selectionObj = window.getSelection();
const rangeObj = selectionObj.getRangeAt(0);
const docFragment = rangeObj.cloneContents();
const div = document.createElement("div");
div.appendChild(docFragment);
return div
}
function buildOption(arr, select = '', key = 'k', val = 'v') {
return arr.map(v => {
if (typeof v === 'string') {
let sel = '';
if (v === select) {
sel = 'selected'
}
return `<option ${sel} value="${v}">${v}</option>`
} else if (typeof v === 'object' || v instanceof Array) {
let sel = '';
if (v[key] === select) {
sel = 'selected'
}
return `<option ${sel} value="${v[key]}">${v[val]}</option>`
}
return ''
}).join('\n');
}
const entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
};
const entityMap2 = Object.keys(entityMap).reduce((pre, cur) => {
pre[entityMap[cur]] = cur
return pre
}, {});
function htmlSpecial(string) {
return String(string).replace(/[&<>"'`=\/]/g, function (s) {
return entityMap[s];
});
}
function decodeHtmlSpecial(string) {
return String(string).replace(/&(amp|lt|gt|quot|#39|#x2F|#x60|#x3D);/ig, function (s) {
return entityMap2[s];
});
}
function base64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
return {
PushContextMenu,
PushIconAction,
PushInitialFn,
initContextMenu,
initIconActions,
request,
parseKey,
tapKeyboard,
readClipboard,
requestEx,
getSelectionElement,
buildOption,
htmlSpecial,
decodeHtmlSpecial,
base64ToUint8Array,
}
})();