PoE Clerk

Makes shopping easy at your friendly neighborhood poe.trade!

/*
    Copyright (C) 2015  Daniel Horowitz

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

// ==UserScript==
// @name         PoE Clerk
// @author       desophos
// @namespace    http://github.com/desophos
// @description  Makes shopping easy at your friendly neighborhood poe.trade!
// @version      1.0.0
// @icon         https://github.com/desophos/PoE-Clerk/raw/master/poe-clerk.png
// @license      GPL 2.0
// @resource     license https://raw.githubusercontent.com/desophos/PoE-Clerk/master/LICENSE.md
// @include      http://poe.trade/search/*
// @require      https://code.jquery.com/jquery-2.1.3.min.js
// @resource     sidebarCSS https://github.com/desophos/PoE-Clerk/raw/master/poe-clerk.css
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_openInTab
// @noframes
// ==/UserScript==

'use strict';

function itemsEqual (item1, item2) {
    return item1.name === item2.name
        && item1.data_hash === item2.data_hash
        && item1.thread === item2.thread
        && item1.league === item2.league;
}

function itemToJson (itemContainer) {
    var itemContainerType = $(itemContainer).prop('tagName');
    if (itemContainerType === 'TBODY') {
        // list view
        return {
            'name': $.trim($(itemContainer).find('.item-cell a.title').text().replace('corrupted ', '')),
            'data_hash': $(itemContainer).find('span.requirements span.click-button').attr('data-hash'),
            'thread': $(itemContainer).find('span.requirements span.click-button').attr('data-thread'),
            // exclude implicit mod
            'mods': $(itemContainer).find('ul.mods:not(.withline) li').map(function () {
                return $(this).html();
            }).get(),
            'league': league,
            //"search_url": window.location.href,
        };
    } else if (itemContainerType === 'DIV') {
        // tiles view
        return {
            'name': $.trim($(itemContainer).find('li.title').html().replace('<br>', ' ')),
            'data_hash': $(itemContainer).find('li.description > span.click-button').attr('data-hash'),
            'thread': $(itemContainer).find('li.description > span.click-button').attr('data-thread'),
            // exclude implicit mod
            'mods': $(itemContainer).find('ul.mods:not(.withline) li').map(function () {
                return $(this).html();
            }).get(),
            'league': league,
            //"search_url": window.location.href,
        };
    } else {
        return {};
    }
}

function viewItem (item) {
    GM_openInTab(
        'http://poe.trade/search?league='
        + item.league
        + '&S='
        + item.data_hash
        + '&thread='
        + item.thread
    );
}

function addToCart (item) {
    if (loadCart().some(function (i) {
        return itemsEqual(i, item);
    })) {
        addedDuplicateToCart(item);
    } else {
        saveItem(item);
    }
}

function addAddToCart() {
    // append add button to each item
    var cart = loadCart();
    for (var i = 0; i < $(itemContainerSelector).length; i++) {
        var itemContainer = $('#item-container-' + i.toString());
        var itemInCart = false;
        for (var j = 0; j < cart.length; j++) {
            if (itemsEqual(itemToJson(itemContainer), cart[j])) {
                itemInCart = true;
                break;
            }
        }
        if (!itemInCart) {
            var addToCartButton = itemContainer.find('span.add-to-cart')
            if (addToCartButton.length === 0) {
                // list view
                var containerToAppendTo = itemContainer.find('span.requirements');
                if (containerToAppendTo.length === 0) {
                    // tiles view
                    containerToAppendTo = itemContainer.find('li.description').last();
                }
                containerToAppendTo.append(
                    '<span class="add-to-cart-separator"> · </span>' +
                    '<span class="click-button add-to-cart">Add to Cart</span>'
                );
            } else if (addToCartButton.css('display') === 'none') {
                addToCartButton.html('Add to Cart');
                addToCartButton.css('display', '');
                addToCartButton.siblings('span.add-to-cart-separator').css('display', '');
            }
        }
    }

    // emit add signal on button click
    $('.add-to-cart').off('click').on('click', function () {
        // store item data
        addToCart(itemToJson($(this).closest(itemContainerSelector)));

        $(this).html('Item added to cart!');
        $(this).fadeOut(2000);
        $(this).siblings('span.add-to-cart-separator').fadeOut(2000);
    });
}

function addedDuplicateToCart (item) {
    alert('This item is already in your cart.');
}

function makeItemRow(item) {
    var mods_div = '<div class="toggle"><ul class="mods">';
    for (var i = 0; i < item.mods.length; i++) {
        mods_div += '<li class="mod">' + item.mods[i] + '</li>';
    }
    mods_div += '</ul></div>';
    return '<tr class="item-row" '
        + 'data-hash="'
        + item.data_hash
        + '" thread="'
        + item.thread
        + '" league="'
        + item.league
        //+ '" search-url="'
        //+ item.search_url
        + '">'
        + '<td class="expander"><button>+</button></td>'
        + '<td class="item-name">'
        + item.name
        + '</td>'
        + '<td class="view-item"><button>view</button></td>'
        + '<td class="remove-item"><button>x</button></td>'
        + '</tr>'
        + '<tr class="mods-row"><td colspan="4">'
        + mods_div
        + '</td></tr>'
        + '<tr class="spacer-row"><td colspan="4"></td></tr>';
}

function refreshCart () {
    var cart = loadCart();
    $('#items-table').html('');

    for (var i = 0; i < cart.length; i++) {
        (function (i) {
            $('#items-table').append(makeItemRow(cart[i]));

            var item_tr = $(
                'tr[data-hash="'
                + cart[i].data_hash
                + '"][thread="'
                + cart[i].thread
                + '"][league="'
                + cart[i].league
                + '"]'
            );

            $(item_tr).find('.expander button').off('click').on('click', function () {
                $(item_tr).next().find('.toggle').first().slideToggle(function () {
                    var expander = $(item_tr.find('.expander button'));
                    if ($(expander).html() === '+') {
                        $(expander).html('&ndash;');
                    } else {
                        $(expander).html('+');
                    }
                });
            });

            $(item_tr).find('.view-item button').off('click').on('click', function () {
                viewItem({
                        'league':    item_tr.attr('league'),
                        'data_hash': item_tr.attr('data-hash'),
                        'thread':    item_tr.attr('thread'),
                });
            });

            $(item_tr).find('.remove-item button').off('click').on('click', function () {
                $(item_tr).next('.mods').remove();
                $(item_tr).next('.spacer').remove();
                item_tr.remove();
                removeItem(cart[i]);
            });
        })(i);
    }
}

function removeItem (item) {
    saveCart(loadCart().filter(function (i) {
        return !(itemsEqual(item, i));
    }));
    refreshCart();
    addAddToCart();
}

function saveItem (item) {
    var cart = loadCart();
    cart.push(item);
    saveCart(cart);
    refreshCart();
}

function loadCart () {
    return JSON.parse(GM_getValue('cart', '[]'));
}

function saveCart (cart) {
    GM_setValue('cart', JSON.stringify(cart));
}

function showSidebar () {
    $('body').append (''
        + '<div id="poe-clerk">'
        + '<div id="header">'
        + '<h5>My Shopping Cart</h5>'
        + '</div>'
        + '<table id="items-table">'
        + '</table>'
        + '<div id="footer"></div>'
        + '</div>'
    );

    var sidebarWidth; // for storing pre-resize width

    function resizeMain () {
        sidebarWidth = $('#poe-clerk').css('width');
        $('html').css ({
            position: 'relative',
            width: 'calc(100% - ' + sidebarWidth + ')',
            left: sidebarWidth,
        });
    }

    resizeMain();
    // resize the rest of the window when the sidebar is resized
    new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {
            // horrible hack:
            // we don't know what style changed,
            // so only resize main if the width changed
            if (sidebarWidth !== $('#poe-clerk').css('width')) {
                resizeMain();
            }
        });
    }).observe($('#poe-clerk').get(0), {
        attributes: true,
        attributeFilter: ['style'],
    });

    refreshCart();
}

function loadPoeClerk () {
    // wait for everything to actually load,
    // because it's not necessarily all rendered by the time onLoad fires
    if (($(itemContainerSelector).length !== 0) && // items
        ($('.loader').length !== 0) && // loader
        ($('form#search select.league').siblings().find('a.chosen-single span').length !== 0) // league field
    ) {
        // save league at first so we get the one they actually searched in,
        // in case they change it afterward
        league = $('form#search select.league').siblings().find('a.chosen-single span').text();

        // add buttons on initial pageload
        addAddToCart();

        // add buttons on ajax item sort
        new MutationObserver(function (mutations) {
            mutations.forEach(function (mutation) {
                if ($(mutation.target).html() === '') {
                    // done loading, so add buttons
                    addAddToCart();
                }
            });
        }).observe($('.loader').get(0), {childList: true});

        showSidebar();
    } else {
        // if everything we want isn't loaded,
        // wait a second and check again
        window.setTimeout(loadPoeClerk, 1000);
    }
}

GM_addStyle(GM_getResourceText('sidebarCSS'));

var itemContainerSelector = '[id^="item-container"]';
var league; // initialized onLoad

loadPoeClerk();