A Universal Script to Re-Enable the Selection and Copying

Enables select, right-click, copy and drag on pages that disable them. Enhanced Feature: Alt Key HyperLink Text Selection

目前为 2021-06-21 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name A Universal Script to Re-Enable the Selection and Copying
  3. // @name:zh-TW A Universal Script to Re-Enable the Selection and Copying
  4. // @version 1.7.8
  5. // @description Enables select, right-click, copy and drag on pages that disable them. Enhanced Feature: Alt Key HyperLink Text Selection
  6. // @description:zh-TW 解除禁止復制、剪切、選擇文本、右鍵菜單的限制。破解鎖右鍵、文字複製、文字選取。增強功能:Alt鍵超連結文字選取。
  7. // @include /^https?\:\/\//
  8. // @grant none
  9. // @run-at document-start
  10. // @namespace https://greasyfork.org/users/371179
  11. // ==/UserScript==
  12. (function $$($) {
  13. 'use strict';
  14. if (document == null || !document.documentElement) return window.requestAnimationFrame($$); // this is tampermonkey bug?? not sure
  15. //console.log('script at', location)
  16.  
  17. function isSupportAdvancedEventListener() {
  18. if ('_b1750' in $) return $._b1750
  19. var prop = 0;
  20. document.createAttribute('z').addEventListener('', null, {
  21. get passive() {
  22. prop++;
  23. },
  24. get once() {
  25. prop++;
  26. }
  27. });
  28. return ($._b1750 = prop == 2);
  29. }
  30.  
  31. function isSupportPassiveEventListener() {
  32. if ('_b1750' in $) return $._b1750
  33. var prop = 0;
  34. document.createAttribute('z').addEventListener('', null, {
  35. get passive() {
  36. prop++;
  37. }
  38. });
  39. return ($._b1750 = prop == 1);
  40. }
  41.  
  42. var getSelection = window.getSelection || Error()(),
  43. requestAnimationFrame = window.requestAnimationFrame || Error()(),
  44. getComputedStyle = window.getComputedStyle || Error()();
  45.  
  46. $ = {
  47. utSelectionColorHack: 'msmtwejkzrqa',
  48. utTapHighlight: 'xfcklblvkjsj',
  49. utLpSelection: 'gykqyzwufxpz',
  50. ksFuncReplacerNonFalse: '___dqzadwpujtct___',
  51. ksEventReturnValue: ' ___ndjfujndrlsx___',
  52. ksSetData: '___rgqclrdllmhr___',
  53.  
  54. mAlert_DOWN: function() {}, //dummy function in case alert replacement is not valid
  55. mAlert_UP: function() {}, //dummy function in case alert replacement is not valid
  56.  
  57. isAnySelection: function() {
  58. var sel = getSelection();
  59. return !sel ? null : (typeof sel.isCollapsed == 'boolean') ? !sel.isCollapsed : (sel.toString().length > 0);
  60. },
  61.  
  62. createCSSElement: function(cssStyle, container) {
  63. var css = document.createElement('style'); //slope: DOM throughout
  64. css.type = 'text/css';
  65. css.innerHTML = cssStyle;
  66. if (container) container.appendChild(css);
  67. return css;
  68. },
  69.  
  70. createFakeAlert: function(_alert) {
  71. if (typeof _alert != 'function') return null;
  72.  
  73. function alert(msg) {
  74. alert.__isDisabled__() ? console.log("alert msg disabled: ", msg) : _alert.apply(this, arguments);
  75. };
  76. alert.toString = () => "function alert() { [native code] }";
  77. return alert;
  78. },
  79.  
  80. createFuncReplacer: function(originalFunc, pName, resFX) {
  81. resFX = function(ev) {
  82. var res = originalFunc.apply(this, arguments);
  83. if (!this || this[pName] != resFX) return res; // if this is null or undefined, or this.onXXX is not this function
  84. if (res === false) return; // return undefined when "return false;"
  85. originalFunc[$.ksFuncReplacerNonFalse] = true;
  86. this[pName] = originalFunc; // restore original
  87. return res;
  88. }
  89. resFX.toString = () => originalFunc.toString();
  90. return resFX;
  91. },
  92.  
  93. listenerDisableAll: function(evt) {
  94. var elmNode = evt.target;
  95. while (elmNode && elmNode.nodeType > 0) { //i.e. HTMLDocument or HTMLElement
  96. var pName = 'on' + evt.type
  97. var f = elmNode[pName];
  98. if (typeof f == 'function' && f[$.ksFuncReplacerNonFalse] !== true) {
  99. var nf = $.createFuncReplacer(f, pName);
  100. nf[$.ksFuncReplacerNonFalse] = true;
  101. elmNode[pName] = nf;
  102. }
  103. elmNode = elmNode.parentNode;
  104. }
  105. },
  106.  
  107. onceCssHighlightSelection: () => {
  108. if (document.documentElement.hasAttribute($.utLpSelection)) return;
  109. $.onceCssHighlightSelection = null
  110. var s = [...document.querySelectorAll('a,p,div,span,b,i,strong,li')].filter(elm => elm.childElementCount === 0); // randomly pick an element containing text only to avoid css style bug
  111. var elm = !s.length ? document.body : s[s.length >> 1];
  112. var selectionStyle = getComputedStyle(elm, ':selection');
  113. if (/^rgba\(\d+,\s*\d+,\s*\d+,\s*0\)$/.test(selectionStyle.getPropertyValue('background-color'))) document.documentElement.setAttribute($.utSelectionColorHack, "");
  114. var elmStyle = getComputedStyle(elm)
  115. if (/^rgba\(\d+,\s*\d+,\s*\d+,\s*0\)$/.test(elmStyle.getPropertyValue('-webkit-tap-highlight-color'))) document.documentElement.setAttribute($.utTapHighlight, "");
  116. },
  117.  
  118. isCurrentClipboardDataReplaced: function(clipboardData) {
  119. var items = clipboardData ? clipboardData.items : null;
  120. if (items && items.length > 0) {
  121. for (var i = 0, l = items.length; i < l; i++) {
  122. if (items[i].type == 'text/plain') return true;
  123. }
  124. }
  125. return false;
  126. },
  127.  
  128. replacementSetData: function(_setData, evt) {
  129. if (typeof _setData != 'function') return;
  130.  
  131. function setData() {
  132. var res = _setData.apply(this, arguments);
  133. try {
  134. if (evt.clipboardData === this && this.setData === setData && evt.cancelable && evt.defaultPrevented === false) {
  135. if ($.isCurrentClipboardDataReplaced(evt.clipboardData)) {
  136. evt.preventDefault();
  137. if (evt.defaultPrevented === true) {
  138. this.setData = _setData;
  139. delete this[$.ksSetData];
  140. }
  141. }
  142. }
  143. } catch (e) {}
  144. return res;
  145. }
  146. setData.toString = () => _setData.toString();
  147. evt.clipboardData.setData = setData;
  148. evt.clipboardData[$.ksSetData] = _setData;
  149. },
  150.  
  151.  
  152. enableSelectClickCopy: function() {
  153.  
  154. $.eyEvts = ['keydown', 'keyup', 'copy', 'contextmenu', 'select', 'selectstart', 'dragstart', 'beforecopy']; //slope: throughout
  155.  
  156. function isDeactivePreventDefault(evt) {
  157. if ($.bypass) return false;
  158. var j = $.eyEvts.indexOf(evt.type);
  159. switch (j) {
  160. case 3:
  161. if (evt.target instanceof Element && (evt.target.textContent || "").trim().length === 0) return false; //exclude elements like video
  162. return true;
  163. case -1:
  164. return false;
  165. case 0:
  166. case 1:
  167. return (evt.keyCode == 67 && (evt.ctrlKey || evt.metaKey) && !evt.altKey && !evt.shiftKey && $.isAnySelection() === true);
  168. case 2:
  169.  
  170. if (!('clipboardData' in evt && 'setData' in DataTransfer.prototype)) return true; // Event oncopy not supporting clipboardData
  171. // see the richtext hack in https://www.cleancss.com/css-beautify/
  172. // see https://developer.mozilla.org/zh-CN/docs/Web/API/Element/copy_event
  173. // see https://w3c.github.io/clipboard-apis/#widl-ClipboardEvent-clipboardData
  174.  
  175. if ($.isCurrentClipboardDataReplaced(evt.clipboardData) == false) { //no replacement data
  176. if (!evt.clipboardData[$.ksSetData] && evt.cancelable && evt.defaultPrevented === false) $.replacementSetData(evt.clipboardData.setData, evt);
  177. return true;
  178. }
  179. var trimedSelectionText = getSelection().toString().trim()
  180. if (trimedSelectionText) {
  181. //there is replacement data and the selection is not empty
  182. console.log("copy event - clipboardData replacement is allowed and the selection is not empty", trimedSelectionText)
  183. return false;
  184. } else {
  185. //there is replacement data and the selection is empty
  186. return false;
  187. }
  188. break; // for js formatting only
  189.  
  190. default:
  191. return true;
  192. }
  193. }
  194.  
  195. Event.prototype.preventDefault = (function(f) {
  196. function preventDefault() {
  197. if (!isDeactivePreventDefault(this)) f.apply(this);
  198. }
  199. preventDefault.toString = () => f.toString();
  200. return preventDefault;
  201. })(Event.prototype.preventDefault);
  202.  
  203. Object.defineProperty(Event.prototype, "returnValue", {
  204. get() {
  205. return $.ksEventReturnValue in this ? this[$.ksEventReturnValue] : true;
  206. },
  207. set(newValue) {
  208. if (!isDeactivePreventDefault(this) && newValue === false) this.preventDefault();
  209. this[$.ksEventReturnValue] = newValue;
  210. },
  211. enumerable: true,
  212. configurable: true
  213. });
  214.  
  215. for (var i = 2, eventsCount = $.eyEvts.length; i < eventsCount; i++) {
  216. document.addEventListener($.eyEvts[i], $.listenerDisableAll, true); // Capture Event; passive:false; expected occurrence COMPLETELY before Target Capture and Target Bubble
  217. }
  218.  
  219. var _alert = window.alert; //slope: temporary
  220. if (typeof _alert == 'function') {
  221. var _mAlert = $.createFakeAlert(_alert);
  222. if (_mAlert) {
  223. var clickBlockingTo = 0;
  224. _mAlert.__isDisabled__ = () => clickBlockingTo > +new Date;
  225. $.mAlert_DOWN = () => (clickBlockingTo = +new Date + 50);
  226. $.mAlert_UP = () => (clickBlockingTo = +new Date + 20);
  227. window.alert = _mAlert
  228. }
  229. }
  230.  
  231. },
  232.  
  233. lpCheckPointer: function(targetElm) {
  234. if (targetElm && targetElm.nodeType == 1 && targetElm.matches('*:hover')) {
  235. if (getComputedStyle(targetElm).getPropertyValue('cursor') == 'pointer' && targetElm.textContent) return true;
  236. }
  237. return false;
  238. },
  239.  
  240. lpFullCancel: function(evt, toPreventDefault) {
  241. $.bypass = true;
  242. !toPreventDefault || evt.preventDefault()
  243. evt.stopPropagation();
  244. evt.stopImmediatePropagation();
  245. $.bypass = false;
  246. },
  247.  
  248. lpMouseDown: function(evt) {
  249. $.lpMouseActive = 0;
  250. if (evt.altKey && !evt.ctrlKey && !evt.metaKey && !evt.shiftKey && evt.button === 0 && $.lpCheckPointer(evt.target)) {
  251. $.lpMouseActive = 1;
  252. $.lpFullCancel(evt, false);
  253. document.documentElement.setAttribute($.utLpSelection, '');
  254. }
  255. },
  256.  
  257. lpMouseUp: function(evt) {
  258. if ($.lpMouseActive == 1) {
  259. $.lpMouseActive = 2;
  260. document.documentElement.removeAttribute($.utLpSelection);
  261. $.lpFullCancel(evt, false);
  262. if ($.onceCssHighlightSelection) window.requestAnimationFrame($.onceCssHighlightSelection);
  263. }
  264. },
  265.  
  266. lpClick: function(evt) {
  267. if ($.lpMouseActive == 2) {
  268. $.lpFullCancel(evt, false);
  269. }
  270. },
  271.  
  272. lpEnable: function() { // this is an optional feature for modern browser
  273. // the built-in browser feature has already disabled the default event behavior, the coding is just to ensure no "tailor-made behavior" occuring.
  274. document.addEventListener('mousedown', $.lpMouseDown, {
  275. capture: true,
  276. passive: true
  277. })
  278. document.addEventListener('mouseup', $.lpMouseUp, {
  279. capture: true,
  280. passive: true
  281. })
  282. document.addEventListener('click', $.lpClick, {
  283. capture: true,
  284. passive: true
  285. })
  286. },
  287.  
  288. mainEnableScript: () => {
  289. var cssStyleOnReady = `
  290.  
  291. *, body *, div, span, body *::before, body *::after, *:hover, *:link, *:visited, *:active , *[style], *[class]{
  292. -khtml-user-select: auto !important; -moz-user-select: auto !important; -ms-user-select: auto !important;
  293. -webkit-touch-callout: default !important; -webkit-user-select: auto !important; user-select: auto !important;
  294. }
  295. *:hover>img[src]{pointer-events:auto !important;}
  296.  
  297. [${$.utSelectionColorHack}] :not(input):not(textarea)::selection{ background-color: Highlight !important; color: HighlightText !important;}
  298. [${$.utSelectionColorHack}] :not(input):not(textarea)::-moz-selection{ background-color: Highlight !important; color: HighlightText !important;}
  299. [${$.utTapHighlight}] *{ -webkit-tap-highlight-color: rgba(0, 0, 0, 0.18) !important;}
  300.  
  301. html[${$.utLpSelection}] *:hover, html[${$.utLpSelection}] *:hover * { cursor:text !important;}
  302. html[${$.utLpSelection}] :not(input):not(textarea)::selection {background-color: rgba(255, 156, 179,0.5) !important;}
  303. html[${$.utLpSelection}] :not(input):not(textarea)::-moz-selection {background-color: rgba(255, 156, 179,0.5) !important;}
  304.  
  305. [scc_emptyblock="2"]{pointer-events:none !important;user-select:none !important;}
  306. img[scc_emptyblock="4"]{
  307. opacity: 0 !important;
  308. padding: 0 !important;
  309. margin: 0 !important;
  310. position: absolute !important;
  311. width: 100% !important;
  312. height: 100% !important;
  313. left: 0 !important;
  314. top: 0 !important;
  315.  
  316. pointer-events:auto; user-select:auto;cursor:inherit;}
  317.  
  318. `.trim();
  319.  
  320. $.enableSelectClickCopy()
  321. $.createCSSElement(cssStyleOnReady, document.documentElement);
  322.  
  323. },
  324.  
  325. mainEvents: (listenerPress, listenerRelease) => {
  326. document.addEventListener("mousedown", listenerPress, true); // Capture Event; (desktop)
  327. document.addEventListener("contextmenu", listenerPress, true); // Capture Event; (desktop&mobile)
  328. document.addEventListener("mouseup", listenerRelease, false); // Bubble Event;
  329. },
  330.  
  331. disableHoverBlock: () => {
  332.  
  333. var nMap = new WeakMap()
  334.  
  335. function elmParam(elm) {
  336.  
  337. var mElm = nMap.get(elm);
  338. if (!mElm) nMap.set(elm, mElm = {});
  339. return mElm;
  340. }
  341.  
  342. function overlapArea(rect1, rect2) {
  343.  
  344. let l1 = {
  345. x: rect1.left,
  346. y: rect1.top
  347. }
  348.  
  349. let r1 = {
  350. x: rect1.right,
  351. y: rect1.bottom
  352. }
  353. let l2 = {
  354. x: rect2.left,
  355. y: rect2.top
  356. }
  357.  
  358. let r2 = {
  359. x: rect2.right,
  360. y: rect2.bottom
  361. }
  362.  
  363. // Area of 1st Rectangle
  364. let area1 = Math.abs(l1.x - r1.x) * Math.abs(l1.y - r1.y);
  365.  
  366. // Area of 2nd Rectangle
  367. let area2 = Math.abs(l2.x - r2.x) * Math.abs(l2.y - r2.y);
  368.  
  369. // Length of intersecting part i.e
  370. // start from max(l1.x, l2.x) of
  371. // x-coordinate and end at min(r1.x,
  372. // r2.x) x-coordinate by subtracting
  373. // start from end we get required
  374. // lengths
  375. let x_dist = Math.min(r1.x, r2.x) - Math.max(l1.x, l2.x);
  376. let y_dist = (Math.min(r1.y, r2.y) - Math.max(l1.y, l2.y));
  377. let areaI = 0;
  378. if (x_dist > 0 && y_dist > 0) {
  379. areaI = x_dist * y_dist;
  380. }
  381.  
  382. return {
  383. area1,
  384. area2,
  385. areaI
  386. };
  387.  
  388.  
  389. }
  390.  
  391. function redirectEvent(event, toElement) {
  392.  
  393. toElement.dispatchEvent(new event.constructor(event.type, event));
  394. event.preventDefault();
  395. event.stopPropagation();
  396. }
  397.  
  398. const floatingBlockHover = new WeakMap();
  399.  
  400. document.addEventListener('mouseenter', function(evt) {
  401.  
  402. if (evt && evt.target && evt.target.parentNode) {} else {
  403. return;
  404. }
  405.  
  406. if (floatingBlockHover.has(evt.target)) return;
  407.  
  408. function setNULL() {
  409. floatingBlockHover.set(evt.target, 1)
  410. }
  411.  
  412. if (evt.target.nodeType != 1) return setNULL();
  413. if ("SVG|IMG|HTML|BODY".indexOf(evt.target.nodeName) >= 0) return setNULL();
  414.  
  415. const targetArea = evt.target.clientWidth * evt.target.clientHeight
  416.  
  417. if (targetArea > 0) {} else {
  418. return setNULL();
  419. }
  420.  
  421. let possibleResults = [];
  422.  
  423. for (const imgElm of document.querySelectorAll('img[src]')) {
  424. const param = elmParam(imgElm)
  425. if (!param.area) {
  426. const area = imgElm.clientWidth * imgElm.clientHeight
  427. if (area > 0) param.area = area;
  428. }
  429. if (param.area > 0) {
  430. if (targetArea > param.area*0.9) possibleResults.push(imgElm)
  431. }
  432. }
  433.  
  434. let i = 0;
  435. let j = 0;
  436. for (const imgElm of possibleResults) {
  437.  
  438. const cmpVal = evt.target.compareDocumentPosition(imgElm)
  439.  
  440. /*
  441.  
  442.  
  443. 1: The two nodes do not belong to the same document.
  444. 2: p1 is positioned after p2.
  445. 4: p1 is positioned before p2.
  446. 8: p1 is positioned inside p2.
  447. 16: p2 is positioned inside p1.
  448. 32: The two nodes has no relationship, or they are two attributes on the same element.
  449.  
  450. */
  451.  
  452. if (cmpVal & 8 || cmpVal & 16) return setNULL();
  453. if (cmpVal & 2) j++; // I<p
  454. else if (cmpVal & 4) break; // I>p
  455.  
  456.  
  457. i++;
  458.  
  459. }
  460.  
  461. // before: j-1 after: j
  462.  
  463. let indexBefore = j - 1;
  464. let indexAfter = j;
  465. if (indexBefore < 0) indexBefore = 0;
  466. if (indexAfter > possibleResults.length - 1) indexAfter = possibleResults.length - 1;
  467.  
  468. for (let i = indexBefore; i <= indexAfter; i++) {
  469. const s = possibleResults[i];
  470. const {
  471. area1,
  472. area2,
  473. areaI
  474. } = overlapArea(evt.target.getBoundingClientRect(), s.getBoundingClientRect())
  475. const criteria = area1 * 0.7
  476. if (areaI>0.9*area2) {
  477. setNULL();
  478. let nImg=document.createElement('img');
  479. nImg.setAttribute('scc_emptyblock','4');
  480. evt.target.insertBefore(nImg,evt.target.firstChild);
  481. nImg.setAttribute('src',s.getAttribute('src'));
  482. evt.target.setAttribute('scc_emptyblock', '7');
  483. !(function(hElm, imgElm){
  484. nImg.addEventListener('click', function(event){ if(event.button!=2)redirectEvent(event, hElm) }, true);
  485. nImg.addEventListener('mousedown', function(event){ if(event.button!=2)redirectEvent(event, hElm) }, true);
  486. nImg.addEventListener('mouseup', function(event){ if(event.button!=2)redirectEvent(event, hElm) }, true);
  487. })(evt.target, s)
  488. return;
  489. }
  490. }
  491.  
  492. setNULL();
  493.  
  494. }, isSupportPassiveEventListener() ? {
  495. passive: true,
  496. capture: true
  497. } : true)
  498.  
  499. }
  500.  
  501. }
  502.  
  503. $.mainEnableScript();
  504.  
  505. if (isSupportAdvancedEventListener()) $.lpEnable(); // top capture event for alt-click
  506.  
  507. $.mainEvents(
  508. function(evt) {
  509. if ($.onceCssHighlightSelection) window.requestAnimationFrame($.onceCssHighlightSelection);
  510. if (evt.button == 2 || evt.type == "contextmenu") $.mAlert_DOWN();
  511. },
  512. function(evt) {
  513. if (evt.button == 2) $.mAlert_UP();
  514. }
  515. );
  516.  
  517. $.disableHoverBlock();
  518.  
  519. console.log('userscript running - To Re-Enable Selection & Copying');
  520.  
  521.  
  522.  
  523.  
  524. })();