Element Fullscreen

Fullscreen any element on a webpage

// ==UserScript==
// @name         Element Fullscreen
// @namespace    http://shitchell.com/
// @include      *
// @description  Fullscreen any element on a webpage
// @author       Shaun Mitchell <shaun@shitchell.com>
// @license      wtfpl
// @grant        GM_addStyle
// @version      0.3
// ==/UserScript==

// Send stuff to the console
var DEBUG = false;

// Key combination to activate element selection (default is Alt-f)
var toggleElementSelectionKey = "F";
var toggleElementSelectionAlt = false;
var toggleElementSelectionCtrl = true;

// Styles and css selectors
var focusedStyle = `box-shadow: 0 3px 6px rgba(0,0,0,0.16),
                                0 3px 6px rgba(0,0,0,0.23),
                                0 3px 6px rgba(255,255,255,0.16),
                                0 3px 6px rgba(255,255,255,0.23) !important;`;
var focusedSelector = "element-f";
var fullScreenStyle = "padding: 1em !important;";
var fullScreenSelector = "element-f-fullscreen";

// Element tracking
var focusedElement = null;

// Start off not running until the defined keypress
var running = false;

function debug()
{
    if (DEBUG)
    {
        let args = Array.from(arguments);
        args.unshift("[Element-F]");
        console.log.apply(null, args);
    }
}

/*
 * Returns a boolean that describes whether or not an element is fullscreened
 */
function isFullScreen()
{
    return document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
}

// Get the element directly under the mouse
// https://stackoverflow.com/a/24540416/794241
function getInnermostHovered()
{
    return [].slice.call(document.querySelectorAll(':hover')).pop();
}

/*
 * Removes any styling from any previously focused elements
 */
function resetFocused()
{
    debug("resetting any focused elements");

    // Remove the focused class from any elements that have it
    document.querySelectorAll(`.${focusedSelector}`).forEach(function(el)
    {
        el.classList.remove(focusedSelector);
        debug("CLEARED: ", el);
    });

    // Unset the focused element
    focusedElement = null;
}

/*
 * Sets the currently hovered element as the focused element and
 * unsets any previously focused elements
 */
function focusElement(el)
{
    // Make sure we're running and the element isn't already focused
    if (!running || el === focusedElement)
    {
        return false;
    }

    // Clear any previously focused elements
    resetFocused();

    // Set the focus to this element
    focusedElement = el;
    debug("FOCUS: ", el);

    // Add the hover class
    el.classList.add(focusedSelector);
}

/*
 * Grabs the element under the cursor and sets it to focused
 */
function setFocusedElement()
{
    if (!running)
    {
        return false;
    }

    let hoveredElement = getInnermostHovered();
    if (hoveredElement !== undefined)
    {
        focusElement(hoveredElement);
    }
}

/*
 * Accepts an event from a listener and then fullscreens the target element
 */
function fullScreenElement(ev)
{
    if (!running)
    {
        return false;
    }

    // Prevent whatever the event would have triggered (like following a link)
    ev.stopPropagation();
    ev.preventDefault();

    if (ev.target !== null)
    {
        let req = ev.target.requestFullScreen || ev.target.webkitRequestFullScreen || ev.target.mozRequestFullScreen;
        if (req !== undefined)
        {
            // Fullscreen the target element
            req.call(ev.target);

            // Add fullscreen class
            ev.target.classList.add(fullScreenSelector);

            // Remove the fullscreen class after we're no longer fullscreened
            ev.target.addEventListener('fullscreenchange', function exitFullScreen() {
                if (!isFullScreen())
                {
                    ev.target.classList.remove(fullScreenSelector);
                    ev.target.removeEventListener('fullscreenchange', exitFullScreen);
                }
            });

            // Make sure the target element has a background set
            ensureBackground(ev.target);

            // Stop running
            running = false;

            // Unset the target element as focused
            resetFocused();
        }
    }
}

/*
 * Returns true if the specified key combination was pressed
 * to initiate element selection.
 */
function validateKeyPress(ev)
{
    if (ev.altKey != toggleElementSelectionAlt)
    {
        return false;
    }
    if (ev.ctrlKey != toggleElementSelectionCtrl)
    {
        return false;
    }
    if (ev.key != toggleElementSelectionKey)
    {
        return false;
    }
    debug("keypress triggered");
    return true;
}

/*
 * Accepts a keypress event and then toggles running (ie, element selection mode)
 */
function toggleRunning(ev)
{
    if (validateKeyPress(ev)) {
      // Prevent whatever the keypress would have triggered
      ev.stopPropagation();
      ev.preventDefault();

      running = !running;
        debug("toggled running =>", running);

        // Remove any focused elements if not running
        if (!running)
        {
            resetFocused();
        }
    }
}

/*
 * Some elements are not set with a background color, defaulting to black in
 * fullscreen mode, which sometimes makes the text hard to read. This method
 * will check to see if an element lacks a background color and, if it does not,
 * temporarily gives it a black or white background based on its text color.
 */
function ensureBackground(el)
{
    debug("testing background for", el);

    // First check to see that there isn't a background already
    let cS = getComputedStyle(el);
    let bgColor = cS.backgroundColor;
    if (bgColor == "rgba(0, 0, 0, 0)") // no background color is set
    {
        let textColor = getComputedStyle(el).color;
        textColor = textColor.substring(textColor.indexOf('(') +1, textColor.length -1).split(', ');
        textColor = {
            'r': textColor[0],
            'g': textColor[1],
            'b': textColor[2]
        };
        bgColor = yiq(textColor.r, textColor.g, textColor.b);

        // Set the background back to nothing after we exit fullscreen
        el.addEventListener('fullscreenchange', function removeBackground()
        {
            debug("potentially removing temporary background from", el);
            // Only run if the screen changed and exited fullscreen mode
            if (!isFullScreen())
            {
                debug("removing temporary background from", el);
                el.style.backgroundColor = null;
                el.removeEventListener('fullscreenchange', removeBackground);
            }
        });
        el.style.backgroundColor = bgColor;
        debug("YIQ: Got bg color", bgColor);
    }
    return bgColor;
}

/*
 * Determines whether black or white is more appropriate for
 * a given color using YIQ computation
 */
function yiq(r, g, b)
{
    let color = Math.round(((parseInt(r) * 299) +
                            (parseInt(g) * 587) +
                            (parseInt(b) * 114)) / 1000);
    return (color > 125) ? 'black' : 'white';
}

(function()
{
    'use strict';

    // Set the style for the actively hovered element
    GM_addStyle(`.${focusedSelector} {
        cursor: crosshair !important;
        ${focusedStyle};
    }`);

    // Set the style for the fullscreened element
    GM_addStyle(`.${fullScreenSelector} {
        overflow: auto !important;
        ${fullScreenStyle};
    }`);

    // Toggle whether or not we're looking for elements based on the defined keypress
    document.body.addEventListener('keydown', toggleRunning);

    // Set the element under the cursor to the focused element (only if running)
    document.body.addEventListener('mousemove', setFocusedElement);

    // Listen for a click and fullscreen that element (only if running)
    document.body.addEventListener('click', fullScreenElement, true);
})();