SteamGifts: Open in pop-in

Opens various links in pop-in window. This includes Giveaway links, links to Steam store and new messages.

// ==UserScript==
// @name        SteamGifts: Open in pop-in
// @namespace   lainscripts_steamgifts_steam_popin
// @description Opens various links in pop-in window. This includes Giveaway links, links to Steam store and new messages.
// @include     *://www.steamgifts.com/*
// @include     *://steamdb.info/*
// @version     4.6
// @grant       none
// @run-at      document-start
// ==/UserScript==
/* jshint esnext: true */
(()=>{
    'use strict';

    let empty = [],
        truncSubdomains = (s) => (s.match(/[^.]+\.[^.]+$/i) || empty)[0],
        domains = {
            SteamGifts : 'steamgifts.com',
            SteamDB : 'steamdb.info'
        },

        inIframe = (
            function()
            {
                try {
                    return window.self !== window.top;
                } catch (e) {
                    return true;
                }
            }
        )(),

        runCodeFor = {
            [domains.SteamGifts]: function()
            {
                let isSteamGifts = (s) => truncSubdomains(s) === domains.SteamGifts,
                    getFirstLevel = (pathname) => (/^\/[^/]+/i.exec(pathname) || empty)[0],
                    isKnown = /^\/giveaways\/|\/(about|account|discussions?|giveaway|go\/comment|legal|messages|roles|support|user)(\/|$)/i,
                    isAboutLegal = /^\/(about|legal)\//i,
                    isDiscussion = /^\/(discussion|go\/comment)\//i,
                    isSearch = /^\/giveaways\/search/i,
                    isSteamStore = /^store\.steampowered\.com$/i,
                    isSteamPackage = /\/(app|sub)\//i;

                // attach styles before document displayed
                (function(style)
                 {
                    let insert = style.sheet.insertRule.bind(style.sheet);
                    style.id = 'popinStyles';
                    style.type = 'text/css';

                    insert('@keyframes spinnerFrames {0% {transform:rotate(0deg)} 100% {transform:rotate(-360deg)}}',0);
                    insert(
                        '#popinSpinner svg {'+(
                            'animation: spinnerFrames linear 1s;'+
                            'animation-iteration-count: infinite;'+
                            'transform-origin: 50% 50%'
                        )+'}', 0);
                    insert(
                        '#popinBackground {'+(
                            'box-sizing: border-box;'+
                            'position: fixed;'+
                            'top: 0;'+
                            'left: 0;'+
                            'padding: 35px 40px;'+
                            'width: 100%;'+
                            'height: 100%;'+
                            'background-color: rgba(0,0,0,0.85);'+
                            'z-index: 1000000;'+
                            'display: none'
                        )+'}', 0);
                    insert(
                        '#popinFrame {'+(
                            'position: relative;'+
                            'width: 100%;'+
                            'height: 100%;'+
                            'border: #aaa 2px solid;'+
                            'border-radius: 3px;'+
                            'background-color: rgba(0,0,0,0.33);'+
                            'z-index: 1'
                        )+'}', 0);
                    insert(
                        '#popinSpinner {'+(
                            'position: fixed;'+
                            'top: 50%;'+
                            'left: 50%;'+
                            'margin-top: -50px;'+
                            'margin-left: -50px;'+
                            'width: 100px;'+
                            'height: 100px;'+
                            'z-index: 2'
                        )+'}', 0);

                    // modify navigation elements when various pages are loaded in the pop-in
                    if (inIframe)
                    {
                        // make page cover the entire window height and reset padding with margin if someone changed them
                        insert(
                            'html, body {'+(
                                'height: 100%;'+
                                'padding: 0!important;'+
                                'margin: 0!important'
                            )+'}', 0);
                        // hide some useless for pop-in blocks
                        insert((
                            '.offer__outer-wrap,'+
                            '.footer__outer-wrap'
                        )+' {display: none}', 0);
                        // make content as heigh as the page + fix for the giveaway pages (they have 1 extra block)
                        if (getFirstLevel(document.location.pathname) === '/giveaway')
                            insert('.page__outer-wrap {min-height: calc(100% - 50px - 208px)}', 0);
                        else
                            insert('.page__outer-wrap {min-height: calc(100% - 50px)}', 0);
                        // display floating account button in the top-right corner (except for the account page)
                        if (getFirstLevel(document.location.pathname) === '/account')
                            insert('header {display: none!important}', 0);
                        else
                        {
                            insert((
                                '.nav__left-container,'+
                                '.nav__button-container--notification'
                            )+' {display: none}', 0);
                            insert('header > nav {padding: 0!important}', 0);
                            insert('.sidebar {top: 25px!important}', 0);
                            insert(
                                'header {'+(
                                    'top: -5px!important;'+
                                    'right: 0;'+
                                    'padding: 0;'+
                                    'position: fixed;'+
                                    'width: auto!important;'+
                                    'background: none;'+
                                    'z-index: 100'
                                )+'}', 0);
                        }
                        // hide Browse category in frames on pages with giveaways lists
                        if (getFirstLevel(document.location.pathname) === '/giveaways')
                            insert('.sidebar__heading:first-of-type, .sidebar__navigation:first-of-type {display: none}', 0);
                        // hide sidebar and padding at the left side for discussions
                        if (getFirstLevel(document.location.pathname) === '/discussion')
                        {
                            insert('.sidebar {display: none}', 0);
                            insert(
                                '.widget-container>div:not(:first-child) {'+(
                                    'padding-left: 0!important;'+
                                    'margin-left: 0!important;'+
                                    'border-left: none!important'
                                )+'}', 0);
                            insert('.page__heading__breadcrumbs a {pointer-events: none}');
                        }
                    }
                })(document.documentElement.appendChild(document.createElement('style')));

                function createSVGSpinner(out_radius, in_radius, width, number_of_lines)
                {
                    let nameSpace = 'http://www.w3.org/2000/svg';
                    let shift = out_radius + width / 2;

                    let svg = document.createElementNS(nameSpace, 'svg');
                    svg.setAttribute('version', '1.1');
                    svg.setAttribute('x', '0px');
                    svg.setAttribute('y', '0px');
                    svg.setAttribute('width', (2 * shift) + 'px');
                    svg.setAttribute('height', (2 * shift) + 'px');
                    svg.setAttribute('style', (
                        'stroke: #fff;'+
                        'stroke-width: ' + width + 'px;'+
                        'stroke-linecap: round'));

                    let g = svg.appendChild(document.createElementNS(nameSpace, 'g'));

                    let num = number_of_lines,
                        line;
                    while (num--) {
                        line = g.appendChild(document.createElementNS(nameSpace, 'line'));
                        line.setAttribute('x1', shift + Math.cos(2 * Math.PI / number_of_lines * num) * in_radius);
                        line.setAttribute('y1', shift + Math.sin(2 * Math.PI / number_of_lines * num) * in_radius);
                        line.setAttribute('x2', shift + Math.cos(2 * Math.PI / number_of_lines * num) * out_radius);
                        line.setAttribute('y2', shift + Math.sin(2 * Math.PI / number_of_lines * num) * out_radius);
                        line.setAttribute('stroke-opacity', 1 / (num + 1));
                    }
                    return svg;
                }

                function popinObj()
                {
                    /* jshint validthis: true */
                    let _self = this;
                    _self.childPopin = null;

                    let spinner = document.createElement('div');
                    spinner.appendChild(createSVGSpinner(45, 25, 10, 10));
                    spinner.id = 'popinSpinner';

                    let popin = document.createElement('div');
                    popin.id = 'popinBackground';

                    let ifrm = null;

                    function createFrame(a)
                    {
                        let url = a.href;
                        let ifr = document.createElement('iframe');
                        ifr.id = 'popinFrame';

                        ifr.onload = function()
                        {
                            spinner.style.zIndex = 0;
                            if (isSteamGifts(a.hostname))
                                _self.childPopin = this.contentDocument.popin;
                        };

                        // Hide spinner if page keeps loading after a 5 seconds
                        setTimeout(
                            function()
                            {
                                if (popin.style.display === 'block')
                                    spinner.style.zIndex = 0;
                            }, 5000
                        );

                        url = url
                            .replace('http:', 'https:')
                            .replace('/store.steampowered.com/', '/steamdb.info/');

                        ifr.src = url;
                        return ifr;
                    }

                    popin.appendChild(spinner);
                    document.body.appendChild(popin);

                    _self.show = function (a)
                    {
                        document.body.style.overflowY = 'hidden';
                        ifrm = createFrame(a);
                        popin.appendChild(ifrm);
                        popin.style.display = 'block';
                    };

                    _self.hide = function ()
                    {
                        let onUpdateParent = false;
                        if (ifrm.src.search('/www.steamgifts.com/') > -1)
                        {
                            // propagate outdated state of parent document and reload it if applicable
                            let deepPopin = _self.childPopin;
                            while(deepPopin && deepPopin.childPopin && !deepPopin.isOutdated())
                                deepPopin = deepPopin.childPopin;
                            if (deepPopin && deepPopin.isOutdated())
                            {
                                document.popin.setIsOutdated();
                                // only pages with active giveaways should be reloaded
                                if (document.location.pathname === '/' ||
                                    isSearch.test(document.location.pathname))
                                {
                                    location.reload();
                                    onUpdateParent = true;
                                }
                            }
                            else
                            {
                                // update points and notifications in parent window
                                let ifDoc = ifrm.contentDocument,
                                    points = '.nav__points',
                                    notifs = '.nav__button[href^="/giveaways/"],.nav__button[href="/messages"]';

                                let pin = ifDoc.querySelector(points);
                                if (pin)
                                    for (let po of document.querySelectorAll(points))
                                        po.textContent = pin.textContent;

                                pin = ifDoc.querySelectorAll(notifs);
                                if (pin.length)
                                {
                                    let replaceNavNotification = function(pTo, to, from) {
                                        if (to && from)
                                            to.textContent = from.textContent;
                                        if (!to && from)
                                            pTo.appendChild(from.cloneNode(true));
                                        if (to && !from)
                                            pTo.removeChild(to);
                                    };
                                    let nnSelector = '.nav__notification';

                                    let pi, po;
                                    for (po of document.querySelectorAll(notifs)) for (pi of pin)
                                        if (po.href === pi.href)
                                        {
                                            po.parentNode.className = pi.parentNode.className;
                                            replaceNavNotification(
                                                po,
                                                po.querySelector(nnSelector),
                                                pi.querySelector(nnSelector)
                                            );
                                        }
                                }
                            }
                        }
                        if (!onUpdateParent)
                        {
                            document.body.style.overflowY = 'auto';
                            popin.removeAttribute('style');
                            spinner.removeAttribute('style');
                        }
                        _self.childPopin = null;
                        ifrm.parentNode.removeChild(ifrm);
                        ifrm = null;
                    };

                    popin.onclick = _self.hide;

                    // parent document state checker (in some cases it's only logical to refresh parent page when popin closed)
                    let isParentOutdated = false;

                    _self.isOutdated = () => isParentOutdated;
                    _self.setIsOutdated = () => isParentOutdated = true;
                }

                document.addEventListener (
                    "DOMContentLoaded", function()
                    {
                        let willMakeTopPageOutdated = '.form__sync-default,.table__remove-default,.js__submit-hide-games';
                        document.popin = new popinObj();

                        document.body.addEventListener(
                            'click', function(e)
                            {
                                let t = e.target;

                                // handle only LMB clicks
                                if (e.button > 0)
                                    return;

                                if (t.closest(willMakeTopPageOutdated))
                                    document.popin.setIsOutdated();

                                // try to drill up to an A element if present
                                t = t.closest('A');

                                // if user clicked a link - try to handle it properly
                                if (t && t.href)
                                {
                                    // handle known links - specific categories on SteamGifts and links to packages on Steam
                                    if ((isSteamGifts(t.hostname) && isKnown.test(t.pathname) && !isSearch.test(t.pathname)) ||
                                        (isSteamStore.test(t.hostname) && isSteamPackage.test(t.pathname)))
                                    {
                                        // do not handle links to pages of the same category as the current one
                                        if ((document.location.pathname !== '/giveaways' &&
                                             (getFirstLevel(t.pathname) === getFirstLevel(document.location.pathname))) ||
                                            (isAboutLegal.test(t.pathname) && isAboutLegal.test(document.location.pathname)) ||
                                            (isDiscussion.test(t.pathname) && isDiscussion.test(document.location.pathname)))
                                            return;
                                        // Workaround for EasySteamGifts: do not handle join button if present
                                        if (t.closest('.esg-join'))
                                            return;
                                        // if we are still here, then
                                        document.popin.show(t);
                                        e.preventDefault();
                                    }
                                    else
                                        // handle unknown links when page is loaded in a frame
                                        if (inIframe)
                                            t.target = '_blank';
                                }
                            }
                        );
                    }
                );
            },

            [domains.SteamDB] : function()
            {
                if (!inIframe)
                    return;

                let isSteamDB = (s) => truncSubdomains(s) === domains.SteamDB;

                document.addEventListener(
                    "DOMContentLoaded", function()
                    {
                        document.body.addEventListener(
                            'click', function(e)
                            {
                                let t = e.target;

                                // handle only LMB clicks
                                if (e.button > 0)
                                    return;

                                // try to drill up to an A element if present
                                t = t.closest('A');

                                // open all third-party domains in a new tab
                                if (t && t.href && !isSteamDB(t.hostname))
                                    t.target = '_blank';
                            }
                        );
                    }
                );
            }
        };

    runCodeFor[truncSubdomains(document.location.hostname)]();
})();