吾輩的博客原文地址: 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 腳本這方面用得不少呢 (๑>ᴗ<๑)