// ==UserScript==
// @name OutOfMilk.com Shopping List Enhancements
// @version 0.1.7
// @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.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
'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
'#ctl00_ctl00_ContentPlaceHolder1_EditProductHistoryDialog1_txtEditProductHistoryDescription { ' +
'width: 350px /* 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></strong></th>
<th><strong>Price</strong></th>
<th><strong></strong></th>
<th><strong></strong></th>
<th><strong></strong></th>
<th colspan="3"></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><#= object.TaxFree #></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.
/*
* 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, 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);
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);
// set initial width of "Product History Management" dialog
// (and wait until dialogs are initialized)
/*
var $ = unsafeWindow.jQuery;
$(document).ready(function () {
$("#manageproducthistoryform").dialog("option", "width", "80%");
}); // $(document).ready()
*/
// 2015-06-30: Changed the way this is being done
// (and, now we have replaceUnsafeFunc() anyway so...)
replaceUnsafeFunc('onload', function(){ $("#manageproducthistoryform").dialog("option", "width", "80%"); });
} catch(err) {
console.log(err);
}
/*
* The End
*/
console.log('User script "' + USERSCRIPT_NAME + '" has completed.');
})();