Rev 72 | Blame | Compare with Previous | Last modification | View Log | RSS feed
let opts = {'init' : false,'mouseDownTarget' : null,'mouseStartX' : 0,'mouseStartY' : 0,'timerId' : {'w1' : 0,'w2' : 0,'w3' : 0,'w4' : 0,},};//-----------------------------------------------------------------------------// window.localStorage is available, but doesn't appear to be initialized// when accessed from content scripts so I'm using message passing and a// background page to get the info.//-----------------------------------------------------------------------------chrome.extension.sendMessage({ "type" : "config" },function (resp) {let key;opts.init = true;for (key in resp) {if (resp.hasOwnProperty(key)) {opts[key] = resp[key];}}debug("Got opts:");debug(opts, true);debug("Walk blocklist");for (let i in opts.blockList) {debug(" blocklist entry: " + i + " -> " + opts.blockList[i]);}let arr;let domain;let href = window.location.href;let flag = false;let hostname = window.location.hostname;debug("window.location.href is " + href);if (window.location.protocol === "file:") {domain = window.location.pathname.match(/^\/([^\/]+)\//)[1];if (opts.blockList[encodeURIComponent(href)] ||opts.blockList[encodeURIComponent(domain)]) {flag = true;}} else if (hostname === 'localhost' ||hostname === '127.0.0.1' ||hostname === '[::1]') {domain = hostname;if (opts.blockList[domain] == 1 ||opts.blockList['localhost'] == 1 ||opts.blockList['127.0.0.1'] == 1 ||opts.blockList['[::1]'] == 1) {flag = true;}} else {arr = hostname.split(".");if (arr.length <= 0) {debug("window.location.hostname is empty");return;}if (opts.blockList[encodeURIComponent(href)]) {domain = href;flag = true;} else {for (i in arr) {if (arr.length < 2) {break;}domain = arr.join(".");debug("Domain walk: " + domain);if (opts.blockList[domain] == 1) {flag = true;break;}arr.shift();}}}if (!domain) {debug("Domain is undefined: " + window.location.href);return;}if (!flag) {debug("Extension enabled for " + domain);document.body.addEventListener("mouseup", autoCopyW, false);document.body.addEventListener("mousedown",function (e) {//debug("StartX = " + e.x + " -- StartY = " + e.y);opts.mouseStartX = e.x;opts.mouseStartY = e.y;opts.mouseDownTarget = e.target;},false);} else {debug("URL is blocklisted, disabling: " + domain);}});const sleep = ((ms) => {return new Promise((resolve) => {setTimeout(() => {resolve();}, ms);});});function padDigits(n, width, z) {width = width || 2;z = z || 0;n = String(n);let arr = [];if (n.length >= width) {return n;}arr.length = width - n.length + 1;return arr.join(z) + n;}function debug(text, standalone) {if (opts.enableDebug) {if (!standalone) {console.debug("Auto-Copy (debug): " + text);} else {console.debug(text);}}}function fade(el, speed) {var timer;if (el.style) {el.style.opacity= '1';}timer = setInterval(function () {el.style.opacity = parseFloat(el.style.opacity) - .02;if (el.style.opacity <= 0) {clearInterval(timer);document.body.removeChild(el);}},speed);}function alertOnCopy(e) {var el, el1;debug("in alertOnCopy");if (opts.nativeAlertOnCopy) {debug("doing native alert");try {s = window.getSelection();text = s.toString();if (opts.trimWhitespace) {text = text.replace(/^\s+|\s+$/g, '');text = text.replace(/[\n\r]+$/g, '');}chrome.extension.sendMessage({"type" : "showNotification","text" : text,"opts" : opts,});if (opts.timerId.w3) {clearTimeout(opts.timerId.w3);opts.timerId.w3 = 0;}opts.timerId.w3 = setTimeout(function () {opts.timerId.w3 = 0;chrome.extension.sendMessage({"type" : "hideNotification",});}, 5000);} catch (e) {debug("Caught error: native alert on copy");debug(e, true);}}if (opts.alertOnCopy) {debug("doing in browser alert");el = document.createElement('div');el.style.fontSize = opts.alertOnCopySize;el.style.fontFamily = 'Helvetica, sans-serif';el.style.fontStyle = 'normal';el.style.fontWeight = 'normal';el.style.boxShadow = '0px 0px 16px 0px #CBCBCB';el.style.border = '1px solid #D9D900';el.style.zIndex = '100000001';el.style.textAlign = 'center';el.style.color = '#444444';el.style.backgroundColor = '#FFFF5C';el.style.position = 'fixed';el.style.borderRadius = '.25em';el.innerHTML = "Auto Copied";el.style.boxSizing = 'content-box';el.style.height = '2em';el.style.lineHeight = '2em';el.style.width = '7em';el.style.padding = '0px';el.style.margin = '0px';debug("location: " + opts.alertOnCopyLocation);if (opts.alertOnCopyLocation === 'topLeft') {el.style.top = '25px';el.style.left = '25px';} else if (opts.alertOnCopyLocation === 'topRight') {el.style.top = '25px';el.style.right = '25px';} else if (opts.alertOnCopyLocation === 'bottomLeft') {el.style.bottom = '25px';el.style.left = '25px';} else {el.style.bottom = '25px';el.style.right = '25px';}document.body.appendChild(el);var duration = parseFloat(opts.alertOnCopyDuration) * 1000;if (isNaN(duration)) {duration = 750;}debug("Fade duration: " + duration);sleep(duration).then(() => {fade(el, 5);});}}function includeComment(params) {params = params || {};if (!params.text) {debug("includeComment: No text supplied");return;}var text;var count = (params.text.split(/\s+/)).length;var comment = '';var flag = true;var url = '';var crlf = (opts.copyAsPlainText) ? "\n" : "<br>";debug("Use modifier to " + opts.includeUrlToggleState + " comment? " +opts.includeUrlToggle +"; modifier key: " + opts.includeUrlToggleModifier);if (opts.includeUrlToggle) {let tflag = false;if (opts.includeUrlToggleState === 'enable' &&modifierKeyActive(params.event, opts.includeUrlToggleModifier)) {debug("Modifier key (" + opts.includeUrlToggleModifier +") was active; adding comment");tflag = true;} else if (opts.includeUrlToggleState === 'disable' &&!modifierKeyActive(params.event, opts.includeUrlToggleModifier)) {debug("Modifier key (" + opts.includeUrlToggleModifier +") was not active; adding comment");tflag = true;}if (!tflag) {debug("Ignoring adding comment due to modifier and state");if (params.merge) {return(params.text);}return('');}}if (opts.includeUrlCommentCountEnabled &&count <= opts.includeUrlCommentCount) {debug("Setting comment flag to false");flag = false;}if (opts.includeUrl && opts.includeUrlText && flag) {comment = opts.includeUrlText;debug("Format: " + comment);if (opts.includeUrlText.indexOf('$title') >= 0) {comment = comment.replace(/\$title/g, document.title);}if (opts.copyAsPlainText) {url = window.location.href;} else {comment = comment.replace(/</g, "<");comment = comment.replace(/>/g, ">");url = '<a title="' + document.title + '" href="' + window.location.href +'">' + window.location.href + '</a>';}if (opts.includeUrlText.indexOf('$url') >= 0) {comment = comment.replace(/\$url/g, url);}if (opts.includeUrlText.indexOf('$crlf') >= 0) {comment = comment.replace(/\$crlf/g, crlf);}var date = new Date();var hr;var timestamp;if (opts.includeUrlText.indexOf('$month') >= 0) {comment = comment.replace(/\$month/g, padDigits(date.getMonth() + 1));}if (opts.includeUrlText.indexOf('$day') >= 0) {comment = comment.replace(/\$day/g, padDigits(date.getDate()));}if (opts.includeUrlText.indexOf('$year') >= 0) {comment = comment.replace(/\$year/g, date.getFullYear());}if (opts.includeUrlText.indexOf('$24hour') >= 0) {comment = comment.replace(/\$24hour/g, padDigits(date.getHours()));}if (opts.includeUrlText.indexOf('$hour') >= 0) {hr = date.getHours();if (hr > 12) {hr -= 12;}comment = comment.replace(/\$hour/g, padDigits(date.getHours()));}if (opts.includeUrlText.indexOf('$minute') >= 0) {comment = comment.replace(/\$minute/g, padDigits(date.getMinutes()));}if (opts.includeUrlText.indexOf('$second') >= 0) {comment = comment.replace(/\$second/g, padDigits(date.getSeconds()));}if (opts.includeUrlText.indexOf('$timestamp') >= 0) {timestamp = date.getFullYear() + '-' + padDigits(date.getMonth() + 1) +'-' + padDigits(date.getDate()) + ' ' + padDigits(date.getHours()) +':' + padDigits(date.getMinutes()) + ':' + padDigits(date.getSeconds());comment = comment.replace(/\$timestamp/g, timestamp);}if (params.merge) {if (opts.prependUrl) {debug("Prepending comment: " + comment);text = comment + crlf + params.text;} else {debug("Postpending comment: " + comment);text = params.text + crlf + comment;}} else {text = comment;}return(text);}if (params.merge) {return(params.text);}return('');}function copyAsPlainText(params) {params = params || {'event' : {}};var s;var text;try {s = window.getSelection();text = s.toString();//-------------------------------------------------------------------------// Don't execute the copy if nothing is selected.//-------------------------------------------------------------------------if (text.length <= 0) {debug("Selection was empty");return;}if (opts.trimWhitespace) {debug("Trimming selectection");text = text.replace(/^\s+|\s+$/g, '');text = text.replace(/[\n\r]+$/g, '');}debug("Got selectection: " + text);if (opts.includeUrl) {text = includeComment({'text' : text, 'merge' : true, 'event' : params.event});}debug("Sending copy as plain text: " + text);chrome.extension.sendMessage({"type" : "reformat","text" : text});} catch (ex) {debug("Caught exception: " + ex);}}//-----------------------------------------------------------------------------// We using a combination of mouse travel and click count to decide how to// handle the clicks. If not mouse travel and a single click then we will not// do anything. If no mouse travel and move than one click means someone could// be double or triple clicking to make a selection so we'll set a delay in// case they are triple clicking. This is to prevent autoCopy from firing// multiple times (for the double click and then again for the triple click).//// If we detect mouse travel and it is a single click then we will call// autoCopy immediately as that should be an indicaton of a selection being// made.//-----------------------------------------------------------------------------function autoCopyW(e) {var x;var y;var mouseTravel = false;debug("Mouse coords: " + e.x + " - " + e.y + " - " + opts.mouseStartX + " - " +opts.mouseStartY);debug("Keyboard modifiers: alt=" + e.altKey + " - ctrl=" + e.ctrlKey +" - shift=" + e.shiftKey);if (opts.mouseStartX && opts.mouseStartY) {x = Math.abs(e.x - opts.mouseStartX);y = Math.abs(e.y - opts.mouseStartY);opts.mouseStartX = 0;opts.mouseStartY = 0;if (x > 3 || y > 3) {debug("Detected mouse drag");mouseTravel = true;}}debug("Click count: " + e.detail + " - mouse travel? " + mouseTravel);if (opts.pasteOnMiddleClick && e.button === 1) {debug("paste requested, calling autoCopy immediately");autoCopyW2(e);} else if (mouseTravel && e.detail === 1) {debug("calling autoCopy immediately");autoCopyW2(e);} else if (!mouseTravel && e.detail === 1) {debug("ignoring click. No mouse travel and click count is one.");return;} else if (mouseTravel && e.detail >= 2) {debug("double click and drag -- calling autoCopy immediately");autoCopyW2(e);} else if (!mouseTravel && e.detail >= 3) {debug("triple click detected -- calling autoCopy immediately");clearTimeout(opts.timerId.w1);opts.timerId.w1 = 0;autoCopyW2(e);} else if (!mouseTravel && e.detail == 2) {//-------------------------------------------------------------------------// We have to wait before calling auto copy when two clicks are// detected to see if there is going to be a triple click.//-------------------------------------------------------------------------debug("timerId? " + opts.timerId.w1);if (!opts.timerId.w1) {debug("Setting timer to call autoCopy -- need to wait and see if there " +"is a triple click.");opts.timerId.w1 = setTimeout(function () {opts.timerId.w1 = 0;autoCopyW2(e);}, 500);}}}//-----------------------------------------------------------------------------// This implements a copy delay//-----------------------------------------------------------------------------function autoCopyW2(e) {if (opts.copyDelay && opts.copyDelayWait >= 0) {debug("Copy delay in effect, waiting " + opts.copyDelayWait + " seconds");let duration = parseFloat(opts.copyDelayWait) * 1000;debug("Copy delay: timerId? " + opts.timerId.w2);if (opts.timerId.w2) {debug("Copy delay: clearing timer");clearTimeout(opts.timerId.w2);}debug("Copy delay: setting timer to call autoCopy");opts.timerId.w2 = setTimeout(function () {opts.timerId.w2 = 0;autoCopy(e);}, duration);} else {debug("Copy delay disabled");autoCopy(e);}}function modifierKeyActive(e, name) {if (name === 'ctrl' && e.ctrlKey && !e.shiftKey && !e.altKey) {return true;} else if (name === 'alt' && e.altKey && !e.ctrlKey && !e.shiftKey) {return true;} else if (name === 'shift' && e.shiftKey && !e.ctrlKey && !e.altKey) {return true;} else if (name === 'ctrlalt' && e.ctrlKey && e.altKey && !e.shiftKey) {return true;} else if (name === 'ctrlshift' && e.ctrlKey && e.shiftKey && !e.altKey) {return true;} else if (name === 'ctrlaltshift' && e.ctrlKey && e.shiftKey && e.altKey) {return true;} else if (name === 'altshift' && e.altKey && e.shiftKey && !e.ctrlKey) {return true;}return false;}//-----------------------------------------------------------------------------// The mouseup target is the element at the point the mouseup event occurs.// It is possible to select text within a text field but have the mouse cursor// move outside of the text field which makes it impossible to tell if a text// field element was involved in the selection. In order to work around this// the mousedown target is used to determine if a text field is involved.//// It is only important if the user wants to exclude selections from text// fields//// The if is always evaluating to false because the message passing hasn't// occurred by the time this code segment is executed. I'm leaving it in// as a placeholder in case localStorage gets initialized directly for content// pages.//-----------------------------------------------------------------------------function autoCopy(e) {var rv;var el;var s;var text;var nodeTypes = { 'input' : false, 'editable' : false };var inputElement = false;var editableElement = false;var duration;if (opts.mouseDownTarget &&opts.mouseDownTarget.nodeName &&(opts.mouseDownTarget.nodeName === "INPUT" ||opts.mouseDownTarget.nodeName === "TEXTAREA")) {debug("Mouse down on input element");inputElement = true;}if (opts.mouseDownTarget && opts.mouseDownTarget.isContentEditable) {debug("Mouse down on content editable element");editableElement = true;}if (opts.enableDebug) {console.debug(opts, true);}debug("Use modifier to " + opts.ctrlState + " extension? " + opts.ctrlToDisable +"; modifier key: " + opts.ctrlToDisableKey);if (opts.ctrlToDisable) {let flag = false;if (opts.ctrlState === 'enable' &&modifierKeyActive(e, opts.ctrlToDisableKey)) {debug("Modifier key (" + opts.ctrlToDisableKey + ") was active; " +"doing copy");flag = true;}if (opts.ctrlState === 'disable' &&!modifierKeyActive(e, opts.ctrlToDisableKey)) {debug("Modifier key (" + opts.ctrlToDisableKey + ") was not active; " +"doing copy");flag = true;}if (!flag) {debug("Ignoring copy due to modifier and state");return;}}if (opts.pasteOnMiddleClick && e.button === 1) {el = e.target;debug("autoCopy: detected paste on middle click");if (((e.target.nodeName === "INPUT"|| e.target.nodeName === "TEXTAREA")&& e.target.type !== "checkbox"&& e.target.type !== "radio"&& !e.target.disabled&& !e.target.readOnly)|| e.target.contentEditable === "true") {rv = document.execCommand('paste');debug("paste rv:" + rv);//-----------------------------------------------------------------------// This is fallback for browsers that don't support execCommand in the// content script//-----------------------------------------------------------------------if (!rv) {try {chrome.extension.sendMessage({"type" : "paste","text" : text},function(text) {var p1;var p2;var start;var end;if (e.target.nodeName === "INPUT"|| e.target.nodeName === "TEXTAREA") {p1 = el.value.substring(0,el.selectionStart);p2 = el.value.substring(el.selectionEnd);el.value = p1 + text + p2;} else if (e.target.contentEditable === "true") {el.innerHTML = el.innerHTML + text;}});} catch (ex) {debug("Caught exception: " + ex);}}} else {debug(e.target.nodeName + " is not editable, cannot perform paste");}return;}if (!opts.enableForContentEditable && editableElement) {debug("Extension is not enabled for content editable elements");return;}if (!opts.enableForTextBoxes && inputElement) {debug("Extension is not enabled for text boxes");return;}s = window.getSelection();debug("selection collapsed? " + s.isCollapsed);text = s.toString();debug("selection collapsed? " + s.isCollapsed + " - length: " + text.length);if (!inputElement && s.isCollapsed) {debug("No selection, ignoring click");return;} else if (inputElement && text.length <= 0) {//-------------------------------------------------------------------------// Chrome is showing collapsed when an input element has selected text.//-------------------------------------------------------------------------debug("(input element) No selection, ignoring click");return;}try {debug("copy as link: " + opts.copyAsLink + "; Modifier to copy as link: " +opts.altToCopyAsLink + "; modifier key: " + opts.altToCopyAsLinkModifier);if (opts.copyAsLink || (opts.altToCopyAsLink &&modifierKeyActive(e, opts.altToCopyAsLinkModifier))) {debug("performing copy as link; modifier key detected: " +opts.altToCopyAsLinkModifier);chrome.extension.sendMessage({"type" : "asLink","text" : text,"href" : window.location.href,"title" : document.title});} else if (opts.copyAsPlainText) {debug("performing copy as plain text");copyAsPlainText({ 'event' : e });} else if (opts.includeUrl) {debug("performing copy with comment");//-----------------------------------------------------------------------// This is needed to clear the clipboard contents. Otherwise, we'll keep// adding to what's on the clipboard in background.js//-----------------------------------------------------------------------rv = document.execCommand("copy");if (opts.trimWhitespace) {debug("Falling back to plain text copy (0x1)");opts.copyAsPlainText = true;copyAsPlainText({ 'event' : e });opts.copyAsPlainText = false;} else if (rv) {text = includeComment({'text' : text,'merge' : false,'event' : e});debug("Got comment: " + text);chrome.extension.sendMessage({"type" : "includeComment","comment" : text,"opts" : opts});} else {debug("Falling back to plain text copy (0x2)");opts.copyAsPlainText = true;copyAsPlainText({ 'event' : e });opts.copyAsPlainText = false;}} else {debug("executing copy");//-----------------------------------------------------------------------// This is needed to clear the clipboard contents. Otherwise, we'll keep// adding to what's on the clipboard in background.js//-----------------------------------------------------------------------rv = document.execCommand("copy");debug("copied: " + rv);if (opts.trimWhitespace || !rv) {debug("Falling back to plain text copy (0x3)");opts.copyAsPlainText = true;copyAsPlainText({ 'event' : e });opts.copyAsPlainText = false;}}alertOnCopy(e);} catch (ex) {debug("Caught exception: " + ex);}if (opts.removeSelectionOnCopy) {debug("Removing selection");s.removeAllRanges();}if (opts.clearClipboard) {if (opts.timerId.w4) {clearTimeout(opts.timerId.w4);opts.timerId.w4 = 0;}duration = parseFloat(opts.clearClipboardWait) * 1000;debug("Setting timer to clear clipboard: " + duration);opts.timerId.w4 = setTimeout(function () {opts.timerId.w4 = 0;debug("Clearing clipboard");chrome.extension.sendMessage({"type" : "clearClipboard",});}, duration);}return;}