Dota 2 & CSGO Lounge item price displayer

Displays an item's lowest price offer from the Steam Community Market and helps to copy an item's name or to quickly open the market listings for an item.


// ==UserScript==
// @name        Dota 2 & CSGO Lounge item price displayer
// @namespace   http://www.enygma.ro
// @version     2.2
// @author      Enygma
// @description Displays an item's lowest price offer from the Steam Community Market and helps to copy an item's name or to quickly open the market listings for an item.
// @license     GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @include     /^http(s)?://(www.)?dota2lounge.com//
// @include     /^http(s)?://(www.)?csgolounge.com//
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// ==/UserScript==

// Determine on which site is the script being executed (dota2lounge or csgolounge)
if (document.URL.match(/^http(s)?:\/\/(www.)?dota2lounge.com\//)) {
    // Dota2 app ID on Steam's community market website.
    appID = 570;

    // Generic item placeholder names used by the d2l website and not existing in the Steam Market.
    genericItemPlaceholderNames = ["Offers", "Any Common", "Any Uncommon", "Any Rare", "Any Mythical", "Any Legendary",
                                   "Any Ancient", "Any Immortal", "Real Money", "+ More", "Any Set"];
} else if (document.URL.match(/^http(s)?:\/\/(www.)?csgolounge.com\//)) {
    // CS:GO app ID on Steam's community market website.
    appID = 730;

    // Generic item placeholder names used by the csgolounge website and not existing in the Steam Market.
    genericItemPlaceholderNames = ["Any Offers", "Real Money", "Dota Items", "TF2 Items"];
}

// Main event listener for hovering items.
document.addEventListener("mouseover", function (event) {
    var itemElement = getItemElement(event);
    if (!itemElement) {
        return;
    }

    attachExtraPanelAndListeners(itemElement);
    getLowestPrice(itemElement);
})

// Get the hovered item, if any.
var getItemElement = function(mouseEvent) {
    var targetElement = mouseEvent.target;
    var itemElement = null;

    // Hover either the item element or its picture (child element).
    if (hasClass(targetElement, "item")) {
        itemElement = targetElement;
    } else if (hasClass(targetElement.parentNode, "item")) {
        itemElement = targetElement.parentNode;
    } else {
        return null;
    }

    // Avoid returning empty item slots.
    var itemNameElement = itemElement.querySelector(".name");
    if (!itemNameElement) {
        return null;
    }

    // Avoid returning generic item placeholders.
    var itemName = getItemName(itemElement);
    if (genericItemPlaceholderNames.indexOf(itemName) > -1) {
        return null;
    }

    return itemElement;
}

// Add to the specified item element an extra panel that contains the price information and a click handler to facilitate copying the item's name 
var attachExtraPanelAndListeners = function(itemElement) {
    var itemNamePanel = itemElement.querySelector(".name");
    // If the extra panel already exists, stop here.
    var extraPanel = itemNamePanel.querySelector(".extraPanel");
    if (extraPanel) {
        return;
    }

    // Otherwise, create our own panel to append... 
    extraPanel = document.createElement('div');
    extraPanel.innerHTML = "<span class='scriptStatus'>Ready</span>" +
        "<button type='button' class='extraButton refreshButton' title='Refresh'/>" +
        "<button type='button' class='extraButton steamMarketListingsButton' title='Show listings for the item on Steam Market'/>";
    extraPanel.setAttribute("class", "extraPanel");

    // ...and append it.
    itemNamePanel.appendChild(extraPanel);
    // Set click event handler for the item's name panel so that the item name can be copied to the clipboard easier.
    itemNamePanel.addEventListener("click", copyItemNameHandler, false);
    // Set click event handler for the refresh button that re-fetches the item's price.
    var refreshButton = extraPanel.querySelector(".refreshButton");
    refreshButton.addEventListener("click", function(event) {
        event.stopPropagation();
        getLowestPrice(itemElement, true);
    }, false);
    // Set click event handler for the Steam market listings button that opens in a new tab.
    var steamMarketListingsButton = extraPanel.querySelector(".steamMarketListingsButton");
    steamMarketListingsButton.addEventListener("click", function(event) {
        event.stopPropagation();
        showSteamMarketListings(itemElement);
    }, false);
}

// Get the lowest price for an item from the Steam market.
var getLowestPrice = function(itemElement, override) {
    var itemNameElement = itemElement.querySelector(".name");
    // Don`t try to get the price if we've already retrieved it.
    if (!override && itemNameElement.querySelector(".scriptStatus").innerHTML != "Ready") {
        return;
    }

    itemNameElement.querySelector(".scriptStatus").innerHTML = "Loading...";
    var url = getSteamMarketListingsURL(itemElement);
    GM_xmlhttpRequest({
        method: "GET",
        url: url,
        onload: function (response) {
            var httpResponse = response.responseText;
            var match = lowestPriceWithFeeRegExp.exec(httpResponse);
            var priceWithFee = "<span class='" + (match ?
                "itemMarketable'>" + match[1] :
                "itemNotMarketable'>Not Marketable") +
                "</span>";
            match = lowestPriceWithoutFeeRegExp.exec(httpResponse);
            var priceWithoutFee = match ? match[1] + " - without fee (seller receives)" : ""; 
            itemNameElement.querySelector(".scriptStatus").innerHTML = "<span title='" + priceWithoutFee + "'>" + priceWithFee + "</span>";
        }
    });
}

// Computes the URL used to access the Steam market listings for a given item.
var getSteamMarketListingsURL = function(itemElement) {
    var itemName = getItemName(itemElement);
    var itemNameEncoded = encodeURIComponent(itemName);
    var url = "http://steamcommunity.com/market/listings/" + appID + "/" + itemNameEncoded + "/";

    return url;
}

// Extract the item's name from a DOM item element.
var getItemName = function(itemElement) {
    var itemNameElement = itemElement.querySelector(".name");
    var itemName = itemNameElement.querySelector("b").innerHTML.trim();

    return itemName;
}

// Cached RegExps used to read the item's value from the Steam page.
var lowestPriceWithFeeRegExp = /<span class="market_listing_price market_listing_price_with_fee">\s*(.*?)\s*<\/span>/i;
var lowestPriceWithoutFeeRegExp = /<span class="market_listing_price market_listing_price_without_fee">\s*(.*?)\s*<\/span>/i;

// Event handler to facilitate copying an item's name.
var copyItemNameHandler = function(event) {
    var clickedElement = event.target;

    // Avoid executing this handler if the "Remove item" button is clicked in a trade.
    if (excludedTags.indexOf(clickedElement.tagName) > -1 || excludedTags.indexOf(clickedElement.parentNode.tagName) > -1) {
        return;
    }

    // Stop the element's parent (item) from getting the click event. This stops the item from being selected.
    event.stopPropagation();

    // Make sure we select the item name element.
    var itemNameElement = clickedElement;
    while (!hasClass(itemNameElement, "name")) {
        itemNameElement = itemNameElement.parentNode;
    }

    // Get and display the item's name.
    var itemName = itemNameElement.querySelector("b").innerHTML.trim();
    window.prompt("Press CTRL+C to copy the item's name:", itemName);
}

// Tags that, if clicked on in an item name panel, should not execute the copyItemNameHandler.
var excludedTags = ["A", "IMG"];

// Opens a new tab with the Steam market listings of a given item.
var showSteamMarketListings = function(itemElement) {
    var url = getSteamMarketListingsURL(itemElement);
    var win = window.open(url, "_blank");
    if (win) {
        // Browser has allowed it to be opened.
        win.focus();
    } else {
        // Broswer has blocked it.
        alert("Please allow popups for this site in order to open the Steam market listings.");
    }
}

// Helper method to check if an element has the specified class name.
var hasClass = function(element, cls) {
    return element && (" " + element.className + " ").indexOf( " " + cls + " " ) > -1;
}

// Style.
GM_addStyle(".itemNotMarketable { color : red } .itemMarketable { color: green } .extraButton { margin-left: 0.3em; vertical-align: top; margin-top: -0.1em; border: 0; padding: 0; width: 16px; height: 16px; }");
GM_addStyle(".refreshButton { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABOUlEQVQ4jc2RsUoDQRCGv32CXECzdjaWRiOCVSA+RdqAL6BFesUXOPUFbCWKJ2thkcRgxCa3cJUEQuCwExRjCi1sxiKXsElO6wz81e58888/sPhlESwxlhNaeP/+zRnO/wCMNaBDIbVZG/ztppLcLYdpgK3uSFgGc05WAnbX7pTcD5FCQ8lyMDOlQ4mQaO8lcRI6Q7wATxsGR32k9YUc9RFtiL1gZsoTq1jk7D3JxLEeFNtKLj6ZqNhWkppHSOvxO3GRFlb3J3mc2VEb/I2mktM3Jtp5UKINgUuProYJoMO+C8jWyGhDXO0hl0Ok2hutma2RcR1UsMjx6ySoA9fJkqGUryu5+UDydSW5azbn1wiJyjFSjp3bO4lrg19opJzacZEhJMIi688juYBkFT+9eRpUGYOmbr6Q9QvwBrFqSdh8NgAAAABJRU5ErkJggg==) no-repeat left center; }");
GM_addStyle(".steamMarketListingsButton { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sIBhUuIxK7S5QAAAFbSURBVDjLY7x27QaDoYGhOQMDwwkGHEBEVJiBgYGBwcLalOHE0dMMb16/ZWBgYGA4f+E8IwM7G+d/fDg4LPD/8VNH/yOD46eO/ufh5P3Pzsb5H68B0tIycM08nLxwDDOEKANgmheumf3//////xeumQ0XJ2hAcFgg3ABkAOPHRsf9Z2LAA0rr8+HsRWvnwGl+IX6EIly29/T0wG3ctm0bShjk5mcjvBAcFojT7/hAbHQcxACYQElx6f+S4tL/e/bvxKtx4rT+/9LSMnDLGLApkpaWwTCopLgUq1cZcNkE88bxU0dRbETHTLDQxQUcbFzgSRcbYEmLymPYE3qYIbsqiWFq2zyGyOgIuOTJawcZCAEWBgYGhlWrV0PieMlCBhkZWQYGBgaGtbuWMKyZt5U4A2CGyCvKMaxZv5KBgYGBYc28DQzLVywnaADjlcvXGYyNjf4zkAHOnj3HDAAt54OFwXNyhAAAAABJRU5ErkJggg==) no-repeat left center; }");