frame

划词弹出附有功能按钮框架

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/534402/1606596/frame.js

  1. // trimmed from https://github.com/barrer/tampermonkey-script/blob/master/translate/translate-dictionary.js
  2.  
  3. ;const {
  4. PushContextMenu,
  5. PushIconAction,
  6. PushInitialFn,
  7. initContextMenu,
  8. initIconActions,
  9. request,
  10. parseKey,
  11. tapKeyboard,
  12. readClipboard,
  13. requestEx,
  14. getSelectionElement,
  15. buildOption,
  16. htmlSpecial,
  17. decodeHtmlSpecial,
  18. base64ToUint8Array,
  19. } = (() => {
  20. if (!Array.prototype.filterAndMapX) {
  21. Object.defineProperty(Array.prototype, 'filterAndMapX', {
  22. value: function (fn) {
  23. const arr = [];
  24. for (const item of this) {
  25. const r = fn(item);
  26. if (r === false) {
  27. continue
  28. }
  29. arr.push(r)
  30. }
  31. return arr;
  32. },
  33. writable: true,
  34. });
  35. }
  36.  
  37. const contextMenuActions = [];
  38. const iconActions = [];
  39. const helperServerHost = GM_getValue('host', 'http://127.0.0.1:9999');
  40. const initialFns = [];
  41.  
  42. function PushInitialFn(...fn) {
  43. initialFns.push(...fn);
  44. }
  45.  
  46. function PushContextMenu(...fn) {
  47. contextMenuActions.push(...fn);
  48. }
  49.  
  50. function PushIconAction(...fn) {
  51. iconActions.push(...fn);
  52. }
  53.  
  54. function initContextMenu() {
  55. contextMenuActions.forEach(menu => {
  56. let fn = menu.action;
  57. if (typeof menu.action === 'string') {
  58. fn = () => {
  59. request('keys=' + parseKey(menu.action), menu.path, menu.hasOwnProperty('call') ? menu.call : null)
  60. }
  61. } else if (typeof menu.action === 'object') {
  62. fn = () => {
  63. request(menu.action, menu.path, menu.hasOwnProperty('call') ? menu.call : null)
  64. }
  65. }
  66. GM_registerMenuCommand(menu.title, () => {
  67. if (self === top) {
  68. fn();
  69. }
  70. }, menu.key);
  71. })
  72. }
  73.  
  74. function initIconActions() {
  75. String.prototype.replaceWithMap = function (m) {
  76. let s = this;
  77. Object.keys(k => {
  78. s = s.replaceAll(k, m[k]);
  79. })
  80. return s
  81. }
  82. /**样式*/
  83. const style = document.createElement('style');
  84. const fontSize = 14; // 字体大小
  85. const iconWidth = 300; // 整个面板宽度
  86. const iconHeight = 400; // 整个面板高度
  87. // 可以自定义的变量 <<<<< (自定义变量修改后把 “@version” 版本号改为 “10000” 防止更新后消失)
  88. const trContentWidth = iconWidth - 16; // 整个面板宽度 - 边距间隔 = 翻译正文宽度
  89. const trContentHeight = iconHeight - 35; // 整个面板高度 - 边距间隔 = 翻译正文高度
  90. const zIndex = '2147483647'; // 渲染图层
  91. style.textContent = GM_getResourceText('style').replaceWithMap({
  92. '${fontSize}': fontSize,
  93. '${zIndex}': zIndex,
  94. '${trContentWidth}': trContentWidth,
  95. '${trContentHeight}': trContentHeight,
  96. });
  97. // iframe 工具库
  98. const iframe = document.createElement('iframe');
  99. let iframeWin = null;
  100. iframe.style.display = 'none';
  101. let icon = document.createElement('tr-icon'); //翻译图标
  102. let content = document.createElement('tr-content'), // 内容面板
  103. contentList = document.createElement('div'), //翻译内容结果集(HTML内容)列表
  104. selected, // 当前选中文本
  105. pageX, // 图标显示的 X 坐标
  106. pageY; // 图标显示的 Y 坐标
  107. // 初始化内容面板
  108. content.appendChild(contentList);
  109.  
  110. // 绑定图标拖动事件
  111. const iconDrag = new Drag(icon);
  112. // 图标数组
  113. let hideCalls = []
  114. // 添加翻译引擎图标
  115. iconActions.forEach(obj => {
  116. const img = document.createElement('img');
  117. img.setAttribute('src', obj.image);
  118. img.setAttribute('alt', obj.name);
  119. img.setAttribute('title', obj.name);
  120. img.setAttribute('icon-id', obj.id);
  121. if (obj.hasOwnProperty('trigger') && obj.trigger) {
  122. img.addEventListener('click', (event) => {
  123. obj.trigger(selected, hideIcon, event);
  124. });
  125. }
  126. icon.appendChild(img);
  127. if (obj.hide) {
  128. hideCalls.push(obj.hide)
  129. }
  130. if (obj.hasOwnProperty('call') && obj.call) {
  131. obj.call(img, content);
  132. }
  133. });
  134. // 添加内容面板(放图标后面)
  135. icon.appendChild(content);
  136. // 添加样式、翻译图标到 DOM
  137. const root = document.createElement('div');
  138. document.documentElement.appendChild(root);
  139. const shadow = root.attachShadow({
  140. mode: 'closed'
  141. });
  142. // iframe 工具库加入 Shadow
  143. shadow.appendChild(iframe);
  144. iframeWin = iframe.contentWindow;
  145.  
  146. const link = document.createElement('link');
  147. link.rel = 'stylesheet';
  148. link.type = 'text/css';
  149. link.href = createObjectURLWithTry(new Blob(['\ufeff', style.textContent], {
  150. type: 'text/css;charset=UTF-8'
  151. }));
  152. // 多种方式最大化兼容:Content Security Policy
  153. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
  154. shadow.appendChild(style); // 内部样式表
  155. shadow.appendChild(link); // 外部样式表
  156. // 翻译图标加入 Shadow
  157. shadow.appendChild(icon);
  158. initialFns.length > 0 && initialFns.forEach(fn => fn(shadow));
  159. // 鼠标事件:防止选中的文本消失
  160. document.addEventListener('mousedown', function (e) {
  161. log('mousedown event:', e);
  162. if (e.target === icon || (e.target.parentNode && e.target.parentNode === icon)) { // 点击了翻译图标
  163. e.preventDefault();
  164. }
  165. });
  166. // 鼠标事件:防止选中的文本消失;显示、隐藏翻译图标
  167. document.addEventListener('mouseup', showIcon);
  168. // 选中变化事件
  169. document.addEventListener('selectionchange', showIcon);
  170. document.addEventListener('touchend', showIcon);
  171.  
  172. /**日志输出*/
  173. function log() {
  174. const debug = false;
  175. if (!debug) {
  176. return;
  177. }
  178. if (arguments) {
  179. for (let i = 0; i < arguments.length; i++) {
  180. console.log(arguments[i]);
  181. }
  182. }
  183. }
  184.  
  185. function isInShadow(ele) {
  186. if (ele === root) {
  187. return true
  188. }
  189. if (!ele || !ele.hasOwnProperty('parentElement') || !ele.parentElement.TagName) {
  190. return false
  191. }
  192. const tag = ele.parentElement.TagName;
  193. if (tag === "TR-CONTENT") {
  194. return true
  195. }
  196. if (!tag) {
  197. return false
  198. }
  199. isInShadow(ele.parentElement);
  200.  
  201. }
  202.  
  203. /**鼠标拖动*/
  204. function Drag(element) {
  205. this.dragging = false;
  206. this.mouseDownPositionX = 0;
  207. this.mouseDownPositionY = 0;
  208. this.elementOriginalLeft = parseInt(element.style.left);
  209. this.elementOriginalTop = parseInt(element.style.top);
  210. const ref = this;
  211. this.startDrag = function (e) {
  212. if (e.target !== element) {
  213. return
  214. }
  215. e.preventDefault();
  216. ref.dragging = true;
  217. ref.startDragTime = new Date().getTime();
  218. ref.mouseDownPositionX = e.clientX;
  219. ref.mouseDownPositionY = e.clientY;
  220. ref.elementOriginalLeft = parseInt(element.style.left);
  221. ref.elementOriginalTop = parseInt(element.style.top);
  222. // set mousemove event
  223. window.addEventListener('mousemove', ref.dragElement);
  224. log('startDrag');
  225. };
  226. this.unsetMouseMove = function () {
  227. // unset mousemove event
  228. window.removeEventListener('mousemove', ref.dragElement);
  229. };
  230. this.stopDrag = function (e) {
  231. e.preventDefault();
  232. ref.dragging = false;
  233. ref.stopDragTime = new Date().getTime();
  234. ref.unsetMouseMove();
  235. log('stopDrag');
  236. };
  237. this.dragElement = function (e) {
  238. log('dragging');
  239. if (!ref.dragging) {
  240. return;
  241. }
  242. e.preventDefault();
  243. // move element
  244. element.style.left = ref.elementOriginalLeft + (e.clientX - ref.mouseDownPositionX) + 'px';
  245. element.style.top = ref.elementOriginalTop + (e.clientY - ref.mouseDownPositionY) + 'px';
  246. log('dragElement');
  247. };
  248. element.onmousedown = this.startDrag;
  249. element.onmouseup = this.stopDrag;
  250. }
  251.  
  252. /**强制结束拖动*/
  253. function forceStopDrag() {
  254. if (iconDrag) {
  255. // 强制设置鼠标拖动事件结束,防止由于网页本身的其它鼠标事件冲突而导致没有侦测到:mouseup
  256. iconDrag.dragging = false;
  257. iconDrag.unsetMouseMove();
  258. }
  259. }
  260.  
  261. // html 字符串转 DOM
  262. /**带异常处理的 createObjectURL*/
  263. function createObjectURLWithTry(blob) {
  264. try {
  265. return iframeWin.URL.createObjectURL(blob);
  266. } catch (error) {
  267. log(error);
  268. }
  269. return '';
  270. }
  271.  
  272. /**显示 icon*/
  273. function showIcon(e) {
  274. log('showIcon event:', e);
  275. let offsetX = 4; // 横坐标翻译图标偏移
  276. let offsetY = 8; // 纵坐标翻译图标偏移
  277. // 更新翻译图标 X、Y 坐标
  278. if (e.pageX && e.pageY) { // 鼠标
  279. log('mouse pageX/Y');
  280. pageX = e.pageX;
  281. pageY = e.pageY;
  282. }
  283. if (e.changedTouches) { // 触屏
  284. if (e.changedTouches.length > 0) { // 多点触控选取第 1 个
  285. log('touch pageX/Y');
  286. pageX = e.changedTouches[0].pageX;
  287. pageY = e.changedTouches[0].pageY;
  288. // 触屏修改翻译图标偏移(Android、iOS 选中后的动作菜单一般在当前文字顶部,翻译图标则放到底部)
  289. offsetX = -26; // 单个翻译图标块宽度
  290. offsetY = 16 * 3; // 一般字体高度的 3 倍,距离系统自带动作菜单、选择光标太近会导致无法点按
  291. }
  292. }
  293. log('selected:' + selected + ', pageX:' + pageX + ', pageY:' + pageY)
  294. if (e.target === icon || (e.target.parentNode && e.target.parentNode === icon)) { // 点击了翻译图标
  295. e.preventDefault();
  296. return;
  297. }
  298. selected = window.getSelection().toString().trim(); // 当前选中文本
  299. log('selected:' + selected + ', icon display:' + icon.style.display);
  300. if (selected && icon.style.display !== 'block' && pageX && pageY) { // 显示翻译图标
  301. log('show icon');
  302. icon.style.top = pageY + offsetY + 'px';
  303. icon.style.left = pageX + offsetX + 'px';
  304. icon.style.display = 'block';
  305. // 兼容部分 Content Security Policy
  306. icon.style.position = 'absolute';
  307. icon.style.zIndex = zIndex;
  308. } else if (!selected && e.target !== document && !isInShadow(e.target)) { // 隐藏翻译图标
  309. log('hide icon');
  310. hideIcon();
  311. }
  312. }
  313.  
  314. /**隐藏 icon*/
  315. function hideIcon() {
  316. icon.style.display = 'none';
  317. content.style.display = 'none';
  318. pageX = 0;
  319. pageY = 0;
  320. forceStopDrag();
  321. if (hideCalls.length > 0) {
  322. hideCalls.forEach(fn => {
  323. fn(icon)
  324. })
  325. }
  326. }
  327. }
  328.  
  329. async function request(data, path = '', call = null) {
  330. data = data ? buildData(data, path) : '';
  331. if (path !== '' && path[0] !== '/') {
  332. path = '/' + path;
  333. }
  334. await GM_xmlhttpRequest({
  335. method: "POST",
  336. url: helperServerHost + path,
  337. data: data,
  338. headers: {
  339. "Content-Type": "application/x-www-form-urlencoded"
  340. },
  341. onload: function (res) {
  342. if (call) {
  343. call(res);
  344. }
  345. },
  346. onerror: function (res) {
  347. console.log(res);
  348. },
  349. onabort: function (res) {
  350. console.log(res);
  351. }
  352. });
  353. }
  354.  
  355. function parseKey(key) {
  356. key = key.trim()
  357. if (key.indexOf('[') > -1) {
  358. return key
  359. }
  360. const keys = key.split(',').map(v => {
  361. v = v.trim()
  362. const vv = v.split(' ')
  363. if (vv.length > 1) {
  364. const k = vv[vv.length - 1]
  365. let kk = vv.slice(0, vv.length - 1)
  366. kk.unshift(k)
  367. return kk
  368. }
  369. return vv
  370. })
  371. return JSON.stringify(keys)
  372. }
  373.  
  374. function buildData(data) {
  375. if (typeof data === 'object') {
  376. data = Object.keys(data).map(k => {
  377. if (data[k] instanceof Array) {
  378. return data[k].map(v => k + '=' + encodeURIComponent(v)).join('&')
  379. }
  380. return k + '=' + encodeURIComponent(data[k])
  381. }).join('&');
  382. }
  383. return data
  384. }
  385.  
  386. async function tapKeyboard(keys) {
  387. await request('keys=' + parseKey(keys))
  388. }
  389.  
  390. async function readClipboard(type = 0) {
  391. const {responseText: text} = await requestEx(helperServerHost + '/clipboard?type=' + (type === 1 ? 'img' : 'text'));
  392. return text
  393. }
  394.  
  395. async function requestEx(url, data = '', options = {}) {
  396. data = buildData(data)
  397. return new Promise((resolve, reject) => {
  398. GM_xmlhttpRequest({
  399. url: url,
  400. data: data,
  401. method: 'GET',
  402. onload: function (res) {
  403. return resolve(res)
  404. },
  405. onerror: function (res) {
  406. reject(res)
  407. },
  408. ...options
  409. });
  410. })
  411.  
  412. }
  413.  
  414. function getSelectionElement() {
  415. const selectionObj = window.getSelection();
  416. const rangeObj = selectionObj.getRangeAt(0);
  417. const docFragment = rangeObj.cloneContents();
  418. const div = document.createElement("div");
  419. div.appendChild(docFragment);
  420. return div
  421. }
  422.  
  423. /**
  424. *
  425. * @param arr ['', {} , [], ...]
  426. * @param select selected value or values
  427. * @param key option value field or index
  428. * @param val option innerText field or index
  429. * @param attr option other attributes
  430. * @returns {*} options string
  431. */
  432. function buildOption(arr, select = '', key = 'k', val = 'v', attr = null) {
  433. const sels = new Set();
  434. if (Array.isArray(select)) {
  435. select.forEach(sels.add);
  436. } else if (select) {
  437. sels.add(select);
  438. }
  439. return arr.map(v => {
  440. let att = '', sel = '';
  441. if (attr !== null && v[attr] && typeof v[attr] === 'object') {
  442. att = Object.keys(v[attr]).map(k => `${k}="${v[attr][k]}"`).join(' ');
  443. }
  444. if (typeof v === 'string' || typeof v === 'number') {
  445. sel = sels.has(v) ? 'selected' : '';
  446. return `<option ${att} ${sel} value="${v}">${v}</option>`
  447. } else if (typeof v === 'object' || v instanceof Array) {
  448. sel = sels.has(v[key]) ? 'selected' : '';
  449. return `<option ${att} ${sel} value="${v[key]}">${v[val]}</option>`
  450. }
  451. return ''
  452. }).join('\n');
  453. }
  454.  
  455. const entityMap = {
  456. '&': '&amp;',
  457. '<': '&lt;',
  458. '>': '&gt;',
  459. '"': '&quot;',
  460. "'": '&#39;',
  461. '/': '&#x2F;',
  462. '`': '&#x60;',
  463. '=': '&#x3D;'
  464. };
  465.  
  466. const entityMap2 = Object.keys(entityMap).reduce((pre, cur) => {
  467. pre[entityMap[cur]] = cur
  468. return pre
  469. }, {});
  470.  
  471. function htmlSpecial(string) {
  472. return String(string).replace(/[&<>"'`=\/]/g, function (s) {
  473. return entityMap[s];
  474. });
  475. }
  476.  
  477. function decodeHtmlSpecial(string) {
  478. return String(string).replace(/&(amp|lt|gt|quot|#39|#x2F|#x60|#x3D);/ig, function (s) {
  479. return entityMap2[s];
  480. });
  481. }
  482.  
  483. function base64ToUint8Array(base64String) {
  484. const padding = '='.repeat((4 - base64String.length % 4) % 4);
  485. const base64 = (base64String + padding)
  486. .replace(/-/g, '+')
  487. .replace(/_/g, '/');
  488.  
  489. const rawData = window.atob(base64);
  490. const outputArray = new Uint8Array(rawData.length);
  491.  
  492. for (let i = 0; i < rawData.length; ++i) {
  493. outputArray[i] = rawData.charCodeAt(i);
  494. }
  495. return outputArray;
  496. }
  497.  
  498. return {
  499. PushContextMenu,
  500. PushIconAction,
  501. PushInitialFn,
  502. initContextMenu,
  503. initIconActions,
  504. request,
  505. parseKey,
  506. tapKeyboard,
  507. readClipboard,
  508. requestEx,
  509. getSelectionElement,
  510. buildOption,
  511. htmlSpecial,
  512. decodeHtmlSpecial,
  513. base64ToUint8Array,
  514. }
  515. })();
  516.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址