DScript / FlexibleHighlight

// ==UserScript==
// @name         FlexibleHighlight
// @namespace    DScript
// @version      0.1.6
// @description  Ctrl+Shift+Click to draw highlight rectangle. Ctrl+Right click to highlight element. Escape to close highlight. Right click on overlay to show settings.
// @author       DScript
// @match        https://*/*
// @match        http://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @updateURL    https://openuserjs.org/meta/DScript/FlexibleHighlight.meta.js
// ==/UserScript==

(function() {
    'use strict';

    var highlight = {
        visible: false,
        preventContext: false,
        overlay: null,
        settings: null,
        target: null,
        padding: GM_getValue('highlight.padding', 6),
        toggle: function(on) {
            highlight.visible = on;
            if(!highlight.overlay) return;
            highlight.overlay.left.style.display =
                highlight.overlay.right.style.display =
                highlight.overlay.top.style.display =
                highlight.overlay.bottom.style.display =
                (on ? 'block' : 'none');

            if(!on) {
                highlight.rectStartX = -1;
                highlight.rectStartY = -1;
                highlight.isDrawingRect = false;
                highlight.target = null;

                document.body.classList.remove('noselect');
            } else {
                document.body.classList.add('noselect');
            }
        },
        createOverlay: function() {
            var div = document.createElement('div');
            div.style.backgroundColor = '#000';
            div.style.opacity = GM_getValue('highlight.opacity', 0.4);
            div.style.position = 'absolute';
            div.style.zIndex = GM_getValue('highlight.zIndex', 10000);
            div.style.display = 'none';
            div.oncontextmenu = function(e) {
                console.log(e);
                if(e.button == 2) {
                    highlight.showSettings();
                    e.preventDefault();
                }
            };
            document.body.appendChild(div);
            return div;
        },
        build: function() {
            if(!!highlight.overlay) return;
            highlight.overlay = {
                left: highlight.createOverlay(),
                right: highlight.createOverlay(),
                top: highlight.createOverlay(),
                bottom: highlight.createOverlay()
            };

            highlight.overlay.left.style.left = 0;
            highlight.overlay.left.style.top = 0;
            highlight.overlay.left.style.height = document.body.clientHeight + 'px';

            highlight.overlay.right.style.top = 0;
            highlight.overlay.right.style.height = document.body.clientHeight + 'px';

            highlight.overlay.top.style.top = 0;

            var style = document.createElement('style');
            document.head.appendChild(style);
            style.sheet.insertRule('.noselect { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }', 0);

            var settings = document.createElement('div');
            settings.style.backgroundColor = '#fff';
            settings.style.border = '1px solid #999';
            settings.style.borderRadius = '4px';
            settings.style.position = 'absolute';
            settings.style.zIndex = GM_getValue('highlight.zIndex', 10000) + 2;
            settings.style.display = 'none';
            settings.style.padding = '20px';
            settings.style.width = '300px';
            settings.style.left = ((document.body.clientWidth - 320) / 2) + 'px';
            settings.style.top = '50px';

            settings.innerHTML  = '<div style="margin-bottom: 3px; font-size: 1.5em;"><b>FlexibleHighlight settings</b></div>';
            settings.innerHTML += '<div>Background opacity</div><div><input type="number" style="width: 100%;" max="1" min="0" step="0.1" class="highlight-opacity" value="' + GM_getValue('highlight.opacity', 0.4) + '"/></div>';
            settings.innerHTML += '<div>Padding around element (px)</div><div><input type="number" style="width: 100%;" max="1" min="30" step="1" class="highlight-padding" value="' + GM_getValue('highlight.padding', 6) + '"/></div>';
            settings.innerHTML += '<div>Background z-index</div><div><input type="number" style="width: 100%;" step="1" class="highlight-z" value="' + GM_getValue('highlight.zIndex', 10000) + '"/></div>';

            var buttons = document.createElement('div');
            settings.appendChild(buttons);

            var saveSettings = document.createElement('button');
            saveSettings.innerText = 'Save';
            saveSettings.type = 'button';
            saveSettings.onclick = highlight.saveSettings;
            buttons.appendChild(saveSettings);

            var cancelSettings = document.createElement('button');
            cancelSettings.innerText = 'Cancel';
            cancelSettings.type = 'button';
            cancelSettings.onclick = highlight.cancelSettings;
            buttons.appendChild(cancelSettings);

            highlight.settings = settings;
            document.body.appendChild(settings);
        },

        showSettings: function() {
            highlight.settings.style.display = 'block';
            highlight.settings.style.top = (50 + document.documentElement.scrollTop) + 'px';
        },

        saveSettings: function() {
            var opacity = highlight.settings.querySelector('.highlight-opacity').value || 0;
            var padding = highlight.settings.querySelector('.highlight-padding').value || 0;
            var zIndex = highlight.settings.querySelector('.highlight-z').value || 0;

            GM_setValue('highlight.opacity', opacity);
            GM_setValue('highlight.padding', padding);
            GM_setValue('highlight.zIndex', zIndex);

            highlight.overlay.left.style.opacity = opacity;
            highlight.overlay.right.style.opacity = opacity;
            highlight.overlay.top.style.opacity = opacity;
            highlight.overlay.bottom.style.opacity = opacity;
            highlight.overlay.left.style.zIndex = zIndex;
            highlight.overlay.right.style.zIndex = zIndex;
            highlight.overlay.top.style.zIndex = zIndex;
            highlight.overlay.bottom.style.zIndex = zIndex;
            highlight.padding = padding;

            highlight.settings.style.display = 'none';
        },

        cancelSettings: function() {
            highlight.settings.querySelector('.highlight-opacity').value = GM_getValue('highlight.opactiy', 0.4);
            highlight.settings.querySelector('.highlight-padding').value = GM_getValue('highlight.padding', 6);
            highlight.settings.querySelector('.highlight-z').value = GM_getValue('highlight.zIndex', 10000);
            highlight.settings.style.display = 'none';
        },

        start: function(target) {

            var style = window.getComputedStyle(target);
            while(style && style.display == 'inline')
            {
                target = target.parentNode;
                style = window.getComputedStyle(target);
            }

            highlight.target = target;
            highlight.parents = -1;
            highlight.highlightElement(target);
        },

        highlightElement: function(el) {
            var left = el.offsetLeft;
            var top = el.offsetTop;
            var p = el.offsetParent;
            while(!!p)
            {
                left += p.offsetLeft;
                top += p.offsetTop;
                p = p.offsetParent;
            }

            left = Math.max(0, left - highlight.padding);
            top = Math.max(0, top - highlight.padding);

            highlight.setRect(left, top, left + el.offsetWidth + (highlight.padding*2), top + el.offsetHeight + (highlight.padding*2));
        },

        parents: -1,
        cycleTarget: function (up) {
            var p = highlight.target;
            if(up) {
                highlight.parents++;
                for(var i = 0; i < highlight.parents; i++)
                    p = p.parentNode;

                if (p == document.body) {
                    highlight.toggle(false);
                } else if(p != null) {
                    highlight.parents++;
                    highlight.highlightElement(p);
                }
            } else {
                highlight.parents = Math.max(highlight.parents-1, -1);
                for(var i = 0; i < highlight.parents; i++)
                    p = p.parentNode;
                highlight.highlightElement(p);
            }
        },

        rectStartX: -1,
        rectStartY: -1,
        isDrawingRect: false,
        startRect: function(x1, y1) {
            highlight.isDrawingRect = true;
            highlight.rectStartX = x1;
            highlight.rectStartY = y1;
            highlight.target = null;
            highlight.setRect(x1, y1, x1, y1);
        },

        updateRect: function(x2, y2) {
            highlight.setRect(highlight.rectStartX, highlight.rectStartY, x2, y2);
        },

        stopRect: function(x2, y2) {
            highlight.isDrawingRect = false;
            highlight.setRect(highlight.rectStartX, highlight.rectStartY, x2, y2);
            highlight.rectStartX = -1;
            highlight.rectStartY = -1;
        },

        setRect: function(x1, y1, x2, y2) {
            highlight.build();

            if(x2 < x1) {
                var x3 = x1;
                x1 = x2;
                x2 = x3;
            }

            if(y2 < y1) {
                var y3 = y1;
                y1 = y2;
                y2 = y3;
            }

            highlight.overlay.left.style.width = x1 + 'px';

            highlight.overlay.right.style.left = x2 + 'px';
            highlight.overlay.right.style.width = (document.body.clientWidth - x2) + 'px';

            highlight.overlay.top.style.left = x1 + 'px';
            highlight.overlay.top.style.height = y1 + 'px';
            highlight.overlay.top.style.width = (x2 - x1) + 'px';

            highlight.overlay.bottom.style.top = y2 + 'px';
            highlight.overlay.bottom.style.left = x1 + 'px';
            highlight.overlay.bottom.style.width = (x2 - x1) + 'px';
            highlight.overlay.bottom.style.height = (document.body.clientHeight - y2) + 'px';

            highlight.toggle(true);
        }
    };

    var getX = function(e) {
        return e.x + document.documentElement.scrollLeft;
    };

    var getY = function(e) {
        return e.y + document.documentElement.scrollTop;
    };

    document.body.onkeyup = function(e) {
        if(e.keyCode == 27)
            highlight.toggle(false);
    };

    document.body.onclick = function(e) {
        if(highlight.isDrawingRect) {
            highlight.stopRect(getX(e), getY(e));
            return;
        }

        if(!e.ctrlKey || !e.shiftKey)
            return;

        e.preventDefault();
        highlight.startRect(getX(e), getY(e));
    };

    document.body.onmousemove = function(e) {
        if(!highlight.isDrawingRect)
            return;

        highlight.updateRect(getX(e), getY(e));
    };

    document.body.onmousedown = function(e) {
        if(e.button != 2 || !e.ctrlKey) return;
        highlight.preventContext = true;
        highlight.start(e.target);
    };

    document.body.oncontextmenu = function(e) {
        if(highlight.preventContext)
            e.preventDefault();
        highlight.preventContext = false;
    };

    document.body.onwheel = function(e) {
        if(!highlight.target) return;
        highlight.cycleTarget(e.deltaY < 0);
        e.preventDefault();
    };

})();