吾辈的博客原文地址: https://blog.rxliuli.com/p/4b2822b2/
吾辈发布了一个油猴脚本,能够直接安装 解除网页限制 以得到更好的使用体验。
在浏览网页时常常会出现的一件事,当吾辈想要复制,忽然发现复制好像没用了?(知乎禁止转载的文章)亦或者是复制的最后多出了一点内容(简书),或者干脆直接不能选中了(360doc)。粘贴时也有可能发现一直粘贴不了(支付宝登陆)。javascript
欲先制敌,必先惑敌。想要解除复制粘贴的限制,就必需要清楚它们是如何实现的。无论如何,浏览器上可以运行的都是 JavaScript,它们都是使用 JavaScript 实现的。实现方式大体都是监听相应的事件(例如 onkeydown
监听 Ctrl-C
),而后作一些特别的操做。html
例如屏蔽复制功能只须要一句代码java
document.oncopy = event => false
是的,只要返回了 false,那么 copy 就会失效。还有一个更讨厌的方式,直接在 body
元素上加行内事件git
<body oncopy="javascript: return false" />
能够看出,通常都是使用 JavaScript 在相应事件中返回 false,来阻止对应事件。那么,既然事件都被阻止了,是否意味着咱们就一筹莫展了呢?吾辈所能想到的解决方案大体有三种方向github
使用 JavaScript 监听事件并自行实现复制/剪切/粘贴功能浏览器
从新实现 addEventListener
而后删除掉网站自定义的事件安全
addEventListener
事件够早,对浏览器默认操做不会生效(密码框禁止复制),并且某些网站也没法破解替换元素并删除 DOM 上的事件属性app
缺点:可能影响到其余类型的事件,复制节点时不会复制使用 addEventListener
添加的事件dom
注:此方法不予演示,缺陷实在过大
总之,若是真的想解除限制,恐怕须要两种方式并用才能够呢函数
思路
copy
事件// 强制复制 document.addEventListener( 'copy', event => { event.clipboardData.setData( 'text/plain', document.getSelection().toString(), ) // 阻止默认的事件处理 event.preventDefault() }, true, )
思路
cut
事件能够看到惟一须要增长的就是须要额外处理可编辑内容了,然而代码量瞬间爆炸了哦
/** * 字符串安全的转换为小写 * @param {String} str 字符串 * @returns {String} 转换后获得的全小写字符串 */ function toLowerCase(str) { if (!str || typeof str !== 'string') { return str } return str.toLowerCase() } /** * 判断指定元素是不是可编辑元素 * 注:可编辑元素并不必定可以进行编辑,例如只读的 input 元素 * @param {Element} el 须要进行判断的元素 * @returns {Boolean} 是否为可编辑元素 */ function isEditable(el) { var inputEls = ['input', 'date', 'datetime', 'select', 'textarea'] return ( el && (el.isContentEditable || inputEls.includes(toLowerCase(el.tagName))) ) } /** * 获取输入框中光标所在位置 * @param {Element} el 须要获取的输入框元素 * @returns {Number} 光标所在位置的下标 */ function getCusorPostion(el) { return el.selectionStart } /** * 设置输入框中选中的文本/光标所在位置 * @param {Element} el 须要设置的输入框元素 * @param {Number} start 光标所在位置的下标 * @param {Number} {end} 结束位置,默认为输入框结束 */ function setCusorPostion(el, start, end = start) { el.focus() el.setSelectionRange(start, end) } /** * 在指定范围内删除文本 * @param {Element} el 须要设置的输入框元素 * @param {Number} {start} 开始位置,默认为当前选中开始位置 * @param {Number} {end} 结束位置,默认为当前选中结束位置 */ function removeText(el, start = el.selectionStart, end = el.selectionEnd) { // 删除以前必需要 [记住] 当前光标的位置 var index = getCusorPostion(el) var value = el.value el.value = value.substr(0, start) + value.substr(end, value.length) setCusorPostion(el, index) } // 强制剪切 document.addEventListener( 'cut', event => { event.clipboardData.setData( 'text/plain', document.getSelection().toString(), ) // 若是是可编辑元素还要进行删除 if (isEditable(event.target)) { removeText(event.target) } event.preventDefault() }, true, )
focus/blur
,以得到最后一个得到焦点的可编辑元素paste
事件/** * 获取到最后一个得到焦点的元素 */ var getLastFocus = (lastFocusEl => { document.addEventListener( 'focus', event => { lastFocusEl = event.target }, true, ) document.addEventListener( 'blur', event => { lastFocusEl = null }, true, ) return () => lastFocusEl })(null) /** * 字符串安全的转换为小写 * @param {String} str 字符串 * @returns {String} 转换后获得的全小写字符串 */ function toLowerCase(str) { if (!str || typeof str !== 'string') { return str } return str.toLowerCase() } /** * 判断指定元素是不是可编辑元素 * 注:可编辑元素并不必定可以进行编辑,例如只读的 input 元素 * @param {Element} el 须要进行判断的元素 * @returns {Boolean} 是否为可编辑元素 */ function isEditable(el) { var inputEls = ['input', 'date', 'datetime', 'select', 'textarea'] return ( el && (el.isContentEditable || inputEls.includes(toLowerCase(el.tagName))) ) } /** * 获取输入框中光标所在位置 * @param {Element} el 须要获取的输入框元素 * @returns {Number} 光标所在位置的下标 */ function getCusorPostion(el) { return el.selectionStart } /** * 设置输入框中选中的文本/光标所在位置 * @param {Element} el 须要设置的输入框元素 * @param {Number} start 光标所在位置的下标 * @param {Number} {end} 结束位置,默认为输入框结束 */ function setCusorPostion(el, start, end = start) { el.focus() el.setSelectionRange(start, end) } /** * 在指定位置后插入文本 * @param {Element} el 须要设置的输入框元素 * @param {String} value 要插入的值 * @param {Number} {start} 开始位置,默认为当前光标处 */ function insertText(el, text, start = getCusorPostion(el)) { var value = el.value el.value = value.substr(0, start) + text + value.substr(start) setCusorPostion(el, start + text.length) } /** * 在指定范围内删除文本 * @param {Element} el 须要设置的输入框元素 * @param {Number} {start} 开始位置,默认为当前选中开始位置 * @param {Number} {end} 结束位置,默认为当前选中结束位置 */ function removeText(el, start = el.selectionStart, end = el.selectionEnd) { // 删除以前必需要 [记住] 当前光标的位置 var index = getCusorPostion(el) var value = el.value el.value = value.substr(0, start) + value.substr(end, value.length) setCusorPostion(el, index) } // 强制粘贴 document.addEventListener( 'paste', event => { // 获取当前剪切板内容 var clipboardData = event.clipboardData var items = clipboardData.items var item = items[0] if (item.kind !== 'string') { return } var text = clipboardData.getData(item.type) // 获取当前焦点元素 // 粘贴的时候获取不到焦点? var focusEl = getLastFocus() // input 竟然不是 [可编辑] 的元素? if (isEditable(focusEl)) { removeText(focusEl) insertText(focusEl, text) event.preventDefault() } }, true, )
脚本全貌
;(function() { 'use strict' /** * 两种思路: * 1. 本身实现 * 2. 替换元素 */ /** * 获取到最后一个得到焦点的元素 */ var getLastFocus = (lastFocusEl => { document.addEventListener( 'focus', event => { lastFocusEl = event.target }, true, ) document.addEventListener( 'blur', event => { lastFocusEl = null }, true, ) return () => lastFocusEl })(null) /** * 字符串安全的转换为小写 * @param {String} str 字符串 * @returns {String} 转换后获得的全小写字符串 */ function toLowerCase(str) { if (!str || typeof str !== 'string') { return str } return str.toLowerCase() } /** * 字符串安全的转换为大写 * @param {String} str 字符串 * @returns {String} 转换后获得的全大写字符串 */ function toUpperCase(str) { if (!str || typeof str !== 'string') { return str } return str.toUpperCase() } /** * 判断指定元素是不是可编辑元素 * 注:可编辑元素并不必定可以进行编辑,例如只读的 input 元素 * @param {Element} el 须要进行判断的元素 * @returns {Boolean} 是否为可编辑元素 */ function isEditable(el) { var inputEls = ['input', 'date', 'datetime', 'select', 'textarea'] return ( el && (el.isContentEditable || inputEls.includes(toLowerCase(el.tagName))) ) } /** * 获取输入框中光标所在位置 * @param {Element} el 须要获取的输入框元素 * @returns {Number} 光标所在位置的下标 */ function getCusorPostion(el) { return el.selectionStart } /** * 设置输入框中选中的文本/光标所在位置 * @param {Element} el 须要设置的输入框元素 * @param {Number} start 光标所在位置的下标 * @param {Number} {end} 结束位置,默认为输入框结束 */ function setCusorPostion(el, start, end = start) { el.focus() el.setSelectionRange(start, end) } /** * 在指定位置后插入文本 * @param {Element} el 须要设置的输入框元素 * @param {String} value 要插入的值 * @param {Number} {start} 开始位置,默认为当前光标处 */ function insertText(el, text, start = getCusorPostion(el)) { var value = el.value el.value = value.substr(0, start) + text + value.substr(start) setCusorPostion(el, start + text.length) } /** * 在指定范围内删除文本 * @param {Element} el 须要设置的输入框元素 * @param {Number} {start} 开始位置,默认为当前选中开始位置 * @param {Number} {end} 结束位置,默认为当前选中结束位置 */ function removeText(el, start = el.selectionStart, end = el.selectionEnd) { // 删除以前必需要 [记住] 当前光标的位置 var index = getCusorPostion(el) var value = el.value el.value = value.substr(0, start) + value.substr(end, value.length) setCusorPostion(el, index) } // 强制复制 document.addEventListener( 'copy', event => { event.clipboardData.setData( 'text/plain', document.getSelection().toString(), ) event.preventDefault() }, true, ) // 强制剪切 document.addEventListener( 'cut', event => { event.clipboardData.setData( 'text/plain', document.getSelection().toString(), ) // 若是是可编辑元素还要进行删除 if (isEditable(event.target)) { removeText(event.target) } event.preventDefault() }, true, ) // 强制粘贴 document.addEventListener( 'paste', event => { // 获取当前剪切板内容 var clipboardData = event.clipboardData var items = clipboardData.items var item = items[0] if (item.kind !== 'string') { return } var text = clipboardData.getData(item.type) // 获取当前焦点元素 // 粘贴的时候获取不到焦点? var focusEl = getLastFocus() // input 竟然不是 [可编辑] 的元素? if (isEditable(focusEl)) { removeText(focusEl) insertText(focusEl, text) event.preventDefault() } }, true, ) function selection() { var dom document.onmousedown = event => { dom = event.target // console.log('点击: ', dom) debugger console.log('光标所在处: ', getCusorPostion(dom)) } document.onmousemove = event => { console.log('移动: ', dom) } document.onmouseup = event => { console.log('松开: ', dom) } } })()
addEventListener
而后删除掉网站自定义的事件该实现来灵感来源自 https://greasyfork.org/en/scr...,几乎完美实现了解除限制的功能
原理很简单,修改原型,从新实现 EventTarget
和 docuement
的 addEventListener
函数
// ==UserScript== // @name 解除网页限制 // @namespace http://github.com/rxliuli // @version 1.0 // @description 破解禁止复制/剪切/粘贴/选择/右键菜单的网站 // @author rxliuli // @include https://www.jianshu.com/* // @grant GM.getValue // @grant GM.setValue // 这里的 @run-at 很是重要,设置在文档开始时就载入脚本 // @run-at document-start // ==/UserScript== ;(() => { /** * 监听全部的 addEventListener, removeEventListener 事件 */ var documentAddEventListener = document.addEventListener var eventTargetAddEventListener = EventTarget.prototype.addEventListener var documentRemoveEventListener = document.removeEventListener var eventTargetRemoveEventListener = EventTarget.prototype.removeEventListener var events = [] /** * 用来保存监听到的事件信息 */ class Event { constructor(el, type, listener, useCapture) { this.el = el this.type = type this.listener = listener this.useCapture = useCapture } } /** * 自定义的添加事件监听函数 * @param {String} type 事件类型 * @param {EventListener} listener 事件监听函数 * @param {Boolean} {useCapture} 是否须要捕获事件冒泡,默认为 false */ function addEventListener(type, listener, useCapture = false) { var _this = this var $addEventListener = _this === document ? documentAddEventListener : eventTargetAddEventListener events.push(new Event(_this, type, listener, useCapture)) $addEventListener.apply(this, arguments) } /** * 自定义的根据类型删除事件函数 * 该方法会删除这个类型下面所有的监听函数,无论数量 * @param {String} type 事件类型 */ function removeEventListenerByType(type) { var _this = this var $removeEventListener = _this === document ? documentRemoveEventListener : eventTargetRemoveEventListener var removeIndexs = events .map((e, i) => (e.el === _this || e.type === arguments[0] ? i : -1)) .filter(i => i !== -1) removeIndexs.forEach(i => { var e = events[i] $removeEventListener.apply(e.el, [e.type, e.listener, e.useCapture]) }) removeIndexs.sort((a, b) => b - a).forEach(i => events.splice(i, 1)) } function clearEvent() { var eventTypes = [ 'copy', 'cut', 'select', 'contextmenu', 'selectstart', 'dragstart', ] document.querySelectorAll('*').forEach(el => { eventTypes.forEach(type => el.removeEventListenerByType(type)) }) } ;(function() { document.addEventListener = EventTarget.prototype.addEventListener = addEventListener document.removeEventListenerByType = EventTarget.prototype.removeEventListenerByType = removeEventListenerByType })() window.onload = function() { clearEvent() } })()
最后,JavaScript hook 技巧是真的不少,果真写 Greasemonkey 脚本这方面用得不少呢 (๑>ᴗ<๑)