OutOfMilk.com Shopping List Enhancements

Collection of HTML/CSS enhancements for various bugs and/or shortcomings of the Shopping Lists page (/ShoppingList.aspx)

当前为 2016-05-06 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          OutOfMilk.com Shopping List Enhancements
// @version       0.2.2
// @description   Collection of HTML/CSS enhancements for various bugs and/or shortcomings of the Shopping Lists page (/ShoppingList.aspx)
// @namespace     https://greasyfork.org/en/users/15562
// @author        Jonathan Brochu (https://greasyfork.org/en/users/15562)
// @license       GPLv3 or later (http://www.gnu.org/licenses/gpl-3.0.en.html)
// @include       http://outofmilk.com/ShoppingList.aspx*
// @include       http://www.outofmilk.com/ShoppingList.aspx*
// @include       https://outofmilk.com/ShoppingList.aspx*
// @include       https://www.outofmilk.com/ShoppingList.aspx*
// @grant         GM_addStyle
// ==/UserScript==

/***
 * History:
 *
 * 0.2.2  Change made:
 *        - For the fix around the "Tax Free" checkbox of the "Edit Product
 *          History" dialog, a jQuery function call was failing so we're now
 *          using its DOM native equivalent.
 *        (2016-05-06)
 * 0.2.1  Changes made:
 *        - Fixed the always-unchecked "Tax Free" checkbox for the dialog
 *          "Edit Product History".
 *        - Updated element id for the "Description" field of the dialog
 *          "Edit Product History".
 *        (2016-01-24)
 * 0.2.0  Changes made:
 *        - Updated the "producthistory-template" template to match column
 *          names recently added by outofmilk.com, while at the same time
 *          disabling the CSS code previously used to display custom column
 *          headers.
 *        - Updated the "producthistory-template" template so that, like the
 *          original one, "Yes" & "No" are used as values for the "Tax Free?"
 *          column instead of "true" & "false".
 *        - Fixed the non-working "Tax Free" checkbox and empty "How Much?"
 *          dropdown list for dialog "shoppingeditpopup".
 *        - Fixed the URL used for the "icon-taxfree.png" image whenever
 *          adding or editing an item that is tax free.
 *        (2015-09-17)
 * 0.1.7  Changes made:
 *        - Updated script for use with the repository [greasyfork.org].
 *        - No change made to the code.
 *        (2015-09-14)
 * 0.1.6  Changes made:
 *        - Kept being annoyed that everytime I added/changed a UPC from the
 *          product history it wouldn't update, so now I re-implement method
 *          saveProductHistory() (from "ProductManagement.js?v=...") with the
 *          added tweaks (after all, I'm the one adding the column). Also,
 *          updated the producthistory template with a new class for that
 *          column.
 *          NOTE: I'll have to watch out for any updates/changes to the site
 *                as I replace the whole function, since obviously I cannot
 *                just patch the existing code. Oh, wait...
 *          NODO: I know they say "eval() is evil()", but what if I'd say the
 *                words "toString()", "String.replace()" and "eval()" in that
 *                particular order... OK, I'll leave that hanging in the air.
 *        - Took the opportunity to fix the call that sets the initial width
 *          of the "Product History Management" dialog, which was failing with
 *          errors of the sort "Permission denied to access property".
 *        (2015-06-30)
 * 0.1.5  Change made:
 *        - Added outofmilk.com as a possible domain for include URLs.
 *        (2015-04-02)
 * 0.1.4  Changes made:
 *        - Changed how the initial width of the "Product History Management"
 *          dialog is set.
 *        - Implemented changes to add a "UPC" column to the product history
 *          table (by changing its jQuery UI dialog template; this is possible
 *          since the web service's "GetAllProductHistoryItems" method already
 *          returns the stored UPC value for each history item).
 *        - Removed keep-alive code since no longer necessary.
 *        (2013-08-19)
 * 0.1.3  Changes made:
 *        - Removed "!important" when setting the (initial) width property of
 *          the "Product History Management" dialog (since the specified width
 *          isn't meant to be permanent).
 *        (2013-04-05)
 * 0.1.2  Changes made:
 *        - Added javascript code to keep the session alive (without the
 *          need to refresh the page).
 *        (2013-04-04)
 * 0.1.1  Changes made:
 *        - Added column names for dialog "Product History Management".
 *        - Changed text alignment for (newly-named) column "Tax Exempt".
 *        (2013-04-04)
 * 0.1.0  First implementation. (2013-04-02)
 *
 */

(function() {
    // constants
    var USERSCRIPT_NAME = 'OutOfMilk.com Shopping List Enhancements';

/*
 * The Payload
 */

    // css definitions
    var css_fixes =
            '@namespace url(http://www.w3.org/1999/xhtml);\n' +
        // Changes & Overrides
            // background overlays for modal dialog with fixed postion
            '.ui-widget-overlay { position: fixed /* original: absolute */ !important ; }\n' +
            // "Product History Management" dialog - increase initial width
            //  '-> now done through javascript
            // //'div[aria-describedby="manageproducthistoryform"] { width: 80% /* original: 600px */ ; }\n' +
            // "Product History Management" dialog - take full parent's width for table within dialog
            'table.producthistory-table { width: 100% /* original: 550px */ !important ; }\n' +
            // "Product History Management" dialog - column headers
            // 2015-09-17: Column headers not needed anymore (done through the template itself)
            ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(1) > strong:before { ' +
            ///        'content: "Item" /* original: (none specified) */ !important ; }\n' +
            ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(3) > strong:before { ' +
            ///        'content: "Tax Exempt" /* original: (none specified) */ !important ; }\n' +
            ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(4) > strong:before { ' +
            ///        'content: "Category" /* original: (none specified) */ !important ; }\n' +
            ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(5) > strong:before { ' +
            ///        'content: "UPC" /* original: (none specified) */ !important ; }\n' +
            'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(6):before { ' +
            ///        'content: "Actions" /* original: (none specified) */ !important ; ' +
                    'text-align: center /* original: left (through inheritance) */ !important ; ' +
                '}\n' +
            'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(5) { ' +
                    'text-align: center /* original: left (through inheritance) */ !important ; ' +
                '}\n' +
            // "Product History Management" dialog - values for column "Tax Exempt" centered horizontally
            'td.producthistorytaxfree { text-align: center /* original: left (through inheritance) */ !important ; }\n' +
            // "Edit Product History" dialog - wider "Description" field
            '#txtEditProductHistoryDescription { ' +
                    'width: 380px /* original: (none specified) */ !important ; }\n' +
        // <END>
            '';
    
    // new "producthistory-template" template
    var templateProductHistory = function() {
/**HEREDOC
    <script type="producthistory-template">
        <# if(this.dataobjects.length > 0) { #>
            <tr>
                <th><strong>Description</strong></th>
                <th><strong>Price</strong></th>
                <th><strong>Tax Free?</strong></th>
                <th><strong>Category</strong></th>
                <th><strong>UPC</strong></th>
                <th colspan="3">Actions</th>
            </tr>
            <# $.each(this.dataobjects, function(i, object) { #>
            <tr>
                <td class="producthistoryid hidden"><#= object.ID #></td>
                <td>
                    <span class="producthistorydescription"><#= trimDescription(object.Description,60,"<acronym title=\"" + object.Description + "\">...</acronym>") #></span>
                </td>
                <td class="producthistoryprice">
                    <span><#= FormatNumberCurrency(object.Price) #></span>
                </td>
                <td class="producthistorytaxfree">
                    <span>
                        <# if (object.TaxFree) { #>
                        Yes
                        <# } else { #>
                        No
                        <# } #>
                    </span>
                </td>
                <td class="producthistorycategory">
                    <span><#= object.CategoryName #></span>
                </td>
                <td class="producthistoryupc">
                    <span><#= object.UPC #></span>
                </td>
                <td>
                    <a href="javascript:void(0);" class="btn-default addproducthistory"><span>Add To List</span></a>
                </td>
                <td>
                    <a href="javascript:void(0);" class="btn-default editproducthistory"><span>Edit</span></a>
                </td>
                <td class="last-column">
                    <a href="javascript:void(0);" class="btn-default deleteproducthistory"><span>Delete</span></a>
                </td>
            </tr>
            <# }); #>
        <# } else { #>
            <tr>
                <td colspan="5">There are no items to display</td>
            </tr>
        <# } #>
    </script>
HEREDOC**/
    };
    
    // new implementation of saveProductHistory()
    var mySaveProductHistory = function($this) {
        if(validateProductHistoryForm()){
            var ID = $(".editproducthistoryid").html();
            var description = $(".editproducthistorydescription").val();
            var price = $(".editproducthistoryprice").val();
            var taxfree = $(".editproducthistorytaxfree input").is(":checked");
            var upc = $(".editproducthistoryupc").val();

            var Params = { "ID": ID, "description":description, "price": price, "upc":upc, "taxfree": taxfree };
            var jQueryParams = JSON.stringify(Params);

            $.ajax({
                type: "POST",
                url: "Services/GenericService.asmx/UpdateProductHistoryItem",
                data: jQueryParams,
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                success: function (msg) {
                    if(msg.d == false){
                            $(".producthistoryitemvalidation").html("An item already exists with this description and price!");
                    } else {
                        var $element = $(".producthistoryid:contains("+ID+")").parents("tr");
                        $element.find(".producthistorydescription").html(description);
                        $element.find(".producthistoryprice").html(FormatNumberCurrency(price));
    /* added --> */     $element.find(".producthistoryupc").html(upc);
                        if(taxfree) {
                            $element.find(".producthistorytaxfree").html("Yes");
                        } else {
                            $element.find(".producthistorytaxfree").html("No");
                        }          

                        $this.dialog("close");
                    }
                },
                failure: function (msg) {
                }
            });
        }
    };
    // of course, I could also do something like:
    /*
        eval('var mySaveProductHistory = ' +
            unsafeWindow.saveProductHistory.toString().replace(/(html\(FormatNumberCurrency\(price\)\);)/, '$1\n$$element.find(".producthistoryupc").html(upc);')
        );
    */
    // but, would blindly patching code be actually better than
    //   replacing a whole function? Yeah, I thought so.

    // 2015-09-17: fix for the "Tax Free" checkbox not working in the "shoppingeditpopup" dialog
    var shoppingeditpopup_fix1 = function() {
        // find the "Tax Free" checkbox and its <div> container
        var $element = $(".shoppingeditpopup"),
            taxfree = $element.find("input#checkbox1"),
            taxfreeParentDiv = taxfree.parent();
        // add the missing "taxfree" class, such that in method
        // ShoppingEngine.updateShoppingItem() the line:
        // ---
        // var taxfree = $element.find(".taxfree input").is(":checked");
        // ---
        // works properly
        taxfreeParentDiv.addClass('taxfree');
    };
    // 2015-09-17: fix for the empty <select> element in the "shoppingeditpopup" dialog
    var shoppingeditpopup_fix2 = function() {
        // find the "How Much?" <select> element for units (removing any children in the process)
        var $element = $(".shoppingeditpopup"),
            editUnits = $element.find("select#drpUnitsEdit").find("option").remove().end(),
            itemUnitOptions = $(".shoppinglistitem").find("select.itemunit").find("option");
        // copy the options for the "shoppinglistitem" dialog
        $.each(itemUnitOptions, function() {
            editUnits.append($("<option />").val($(this).val()).text($(this).text()));
        });
    };
    // 2015-09-17: methods ShoppingEngine.addShoppingItem() & ShoppingEngine.updateShoppingItem()
    //             don't use proper links for "icon-taxfree.png"; fix that
    var iconTaxFreeURLs_fix = function() {
        // patch using .toString() & eval()
        $.each(['addShoppingItem', 'updateShoppingItem'], function() {
            eval("ShoppingEngine." + this + " = " + ShoppingEngine[this].toString().replace("src='Images/icon-taxfree.png", "src='\"+ STATIC_URL +\"images/icon-taxfree.png'"));
        });
    };
    // 2016-01-24: fix for the missing "producthistorytaxfree" class in the "editproducthistoryform" dialog
    var editproducthistory_fix1 = function() {
        // find the "Tax Free" <input type="checkbox"> element and its <td> parent
        var taxfree = $("#txtEditProductHistorytaxfree"),
            taxfreeParentTD = taxfree.parent();
        // add the missing "producthistorytaxfree" class, so that the lines
        //   $(".editproducthistorytaxfree input").attr("checked", true);
        //   $(".editproducthistorytaxfree input").attr("checked", false);
        // work as intended
        taxfreeParentTD.addClass('editproducthistorytaxfree');
    };
    // 2016-01-24: fix for the "Tax Free" checkbox being always unchecked in the "editproducthistoryform" dialog
    var editproducthistory_fix2 = function() {
        $(".manageproducts").on("click", function() {
            $(".editproducthistory").off();
            $(".editproducthistory").on("click", function() {
                var $parent = $(this).parents("tr");
                var ID = $parent.find(".producthistoryid").html();
                var description = $parent.find(".producthistorydescription").html();
                var price = $parent.find(".producthistoryprice").html();
                var Params = { "ID": ID };
                var jQueryParams = JSON.stringify(Params);
                $.growlUI('<img src="' + STATIC_URL + 'images/monster-help.png" />Please Wait...', 'Loading Product History item...');
                $.ajax({
                    type: "POST",
                    url: "Services/GenericService.asmx/GetProductHistoryItem",
                    data: jQueryParams,
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    success: function (msg) {
                        $(".editproducthistorydescription").val(msg.d.Description);
                        $(".editproducthistoryprice").val(FormatNumber(msg.d.Price));
                        $(".editproducthistoryid").html(msg.d.ID);
                        $(".editproducthistoryupc").val(msg.d.UPC);
                        $(".editproducthistorytaxfree input").removeAttribute("checked");
                        if (msg.d.TaxFree) {
                            $(".editproducthistorytaxfree input").checked = true;
                        } else {
                            $(".editproducthistorytaxfree input").checked = false;
                        }
                        $("#editproducthistoryform").find('input').keypress(function (e) {
                            if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
                                $(".ui-dialog[aria-labelledby='ui-dialog-title-editproducthistoryform']").find('.ui-dialog-buttonpane').find('button:first').click();
                                return false;
                            }
                        });
                        $("#editproducthistoryform").dialog("open");
                    },
                    failure: function (msg) {
                    }
                });
            });
        });
    };

/*
 * The Tools
 */

    // heredoc parser
    var getHeredoc = function(container, identifier) {
        // **WARNING**: Inputs not filtered (e.g. types, illegal chars within regex, etc.)
        var re = new RegExp("/\\*\\*" + identifier + "[\\n\\r]+[\\s\\S]*?[\\n\\r]+" + identifier + "\\*\\*/", "m");
        var str = container.toString();
        str = re.exec(str).toString();
        str = str.replace(new RegExp("/\\*\\*" + identifier + "[\\n\\r]+",'m'),'').toString();
        return str.replace(new RegExp("[\\n\\r]+" +identifier + "\\*\\*/",'m'),'').toString();
    };
    
    // template substitution
    var replaceDialogTemplate = function(templateName, newContent) {
        var scripts = document.getElementsByTagName('script');
        if (scripts.length > 0) {
            for (var i = 0; i < scripts.length; i++) {
                if (scripts[i].getAttribute('type') == templateName) {
                    // replace template content
                    scripts[i].innerHTML = newContent.toString().replace(/^[\r\n\s]*<script[^>]*>|<\/script>[\r\n\s]*$/g, '');
                    return;
                }
            }
        }
    };
    
    // code injection
    var injectCode = function(code){
        var tmpScript = document.createElement('script');
        tmpScript.id = '__iC_script-'+Math.random().toString().slice(2);
        if (arguments.length > 1) {
            tmpScript.id = tmpScript.id + '_' + /[$_a-zA-Z][$_a-zA-Z0-9]*/.exec(arguments[1])[0] || tmpScript.id;
        }
        tmpScript.type = 'text/javascript';
        tmpScript.textContent = (function() {
            return [
                ';('+(function () {
                    /*code*/
                    var thisScript = document.getElementById('/*scriptId*/');
                    if (thisScript) { thisScript.parentNode.removeChild(thisScript); } // <-- oh no, you didn't!!
                }).toString()+')();',
                { k: 'code', v: (typeof(code) == 'string' && code.trim().length > 0 ? code : '/*failed*/') },
                { k: 'scriptId', v: tmpScript.id }
            ].reduce(function(base, mapping){
                return base.replace('/*'+mapping.k+'*/', mapping.v);
             });
        })();
        document.head.appendChild(tmpScript);
    };
    var injectFuncCode = function(func){
        if (typeof(func) !== 'function') return;
        if (arguments.length > 1) {
            injectCode('('+func.toString()+')();', arguments[1]);
        } else {
            injectCode('('+func.toString()+')();');
        }
    };
    
    // code injection, specialized
    var replaceUnsafeFunc = function(targetName, newFuncImpl){
        // inspiration: https://greasyfork.org/en/scripts/2599-gm-2-port-function-override-helper/code
        var tmpScript = document.createElement('script');
        tmpScript.id = '__rUF_script-'+Math.random().toString().slice(2);
        if (arguments.length > 2) {
            tmpScript.id = tmpScript.id + '_' + /[$_a-zA-Z][$_a-zA-Z0-9]*/.exec(arguments[2])[0] || tmpScript.id;
        }
        tmpScript.type = 'text/javascript';
        tmpScript.textContent = (function() {
            return [
                ';('+(function () {
                    window/*target*/ = /*newFunc*/window;
                    var thisScript = document.getElementById('/*scriptId*/');
                    if (thisScript) { thisScript.parentNode.removeChild(thisScript); } // <-- oh no, you didn't!!
                }).toString()+')();',
                { k: 'target',   v: '.'+(typeof(targetName) == 'string' && targetName.trim().length > 0 ? targetName : '_void') },
                { k: 'newFunc',  v: (typeof(newFuncImpl) == 'function' ? newFuncImpl : function(){}).toString()+';//' },
                { k: 'scriptId', v: tmpScript.id }
            ].reduce(function(base, mapping){
                return base.replace('/*'+mapping.k+'*/', mapping.v);
             });
        })();
        document.head.appendChild(tmpScript);
    };
    
    // reference some outside objects
    window.console = window.console || (function() {
        if (typeof(unsafeWindow) == 'undefined') return { 'log': function() {} };
        return unsafeWindow.console;
    })();
    
    // self-explanatory
    document.addStyle = function(css) {
        if (typeof(GM_addStyle) != 'undefined') {
            GM_addStyle(css);
        } else {  
            var heads = this.getElementsByTagName('head');
            if (heads.length > 0) {
                var node = this.createElement('style');
                node.type = 'text/css';
                node.appendChild(this.createTextNode(css));
                heads[0].appendChild(node); 
            }
        }
    };

/*
 * The Action
 */

    // css injection
    document.addStyle(css_fixes);
    
    // javascript patching
    try {
        // replace template "producthistory-template"
        replaceDialogTemplate('producthistory-template', getHeredoc(templateProductHistory, 'HEREDOC'));
        
        // replace saveProductHistory()
        replaceUnsafeFunc('saveProductHistory', mySaveProductHistory, 'mySaveProductHistory');
        
        // set initial width of "Product History Management" dialog
        replaceUnsafeFunc('onload', function(){ $("#manageproducthistoryform").dialog("option", "width", "80%"); }, 'onload');
        
        // 2015-09-17: fixes for the "shoppingeditpopup" dialog
        injectFuncCode(shoppingeditpopup_fix1, 'shoppingeditpopup_fix1');
        injectFuncCode(shoppingeditpopup_fix2, 'shoppingeditpopup_fix2');
        
        // 2016-01-24: fixes for the "editproducthistoryform" dialog
        injectFuncCode(editproducthistory_fix1, 'editproducthistory_fix1');
        injectFuncCode(editproducthistory_fix2, 'editproducthistory_fix2');
        
        // 2015-09-17: fixes for "icon-taxfree.png" URLs
        injectFuncCode(iconTaxFreeURLs_fix, 'iconTaxFreeURLs_fix');
    } catch(err) {
        console.log(err);
    }

/*
 * The End
 */

    console.log('User script "' + USERSCRIPT_NAME + '" has completed.');
})();