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 提交的版本,檢視 最新版本

// ==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://gf.qytechs.cn/en/users/15562
// @author        Jonathan Brochu (https://gf.qytechs.cn/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 [gf.qytechs.cn].
 *        - 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://gf.qytechs.cn/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.');
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址