OutOfMilk.com Shopping List Enhancements

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

当前为 2016-01-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name OutOfMilk.com Shopping List Enhancements
  3. // @version 0.2.0
  4. // @description Collection of HTML/CSS enhancements for various bugs and/or shortcomings of the Shopping Lists page (/ShoppingList.aspx)
  5. // @namespace https://greasyfork.org/en/users/15562
  6. // @author Jonathan Brochu (https://greasyfork.org/en/users/15562)
  7. // @license GPLv3 or later (http://www.gnu.org/licenses/gpl-3.0.en.html)
  8. // @include http://outofmilk.com/ShoppingList.aspx*
  9. // @include http://www.outofmilk.com/ShoppingList.aspx*
  10. // @include https://outofmilk.com/ShoppingList.aspx*
  11. // @include https://www.outofmilk.com/ShoppingList.aspx*
  12. // @grant GM_addStyle
  13. // ==/UserScript==
  14.  
  15. /***
  16. * History:
  17. *
  18. * 0.2.0 Changes made:
  19. * - Updated the "producthistory-template" template to match column
  20. * names recently added by outofmilk.com, while at the same time
  21. * disabling the CSS code previously used to display custom column
  22. * headers.
  23. * - Updated the "producthistory-template" template so that, like the
  24. * original one, "Yes" & "No" are used as values for the "Tax Free?"
  25. * column instead of "true" & "false".
  26. * - Fixed the non-working "Tax Free" checkbox and empty "How Much?"
  27. * dropdown list for dialog "shoppingeditpopup".
  28. * - Fixed the URL used for the "icon-taxfree.png" image whenever
  29. * adding or editing an item that is tax free.
  30. * (2015-09-17)
  31. * 0.1.7 Changes made:
  32. * - Updated script for use with the repository [greasyfork.org].
  33. * - No change made to the code.
  34. * (2015-09-14)
  35. * 0.1.6 Changes made:
  36. * - Kept being annoyed that everytime I added/changed a UPC from the
  37. * product history it wouldn't update, so now I re-implement method
  38. * saveProductHistory() (from "ProductManagement.js?v=...") with the
  39. * added tweaks (after all, I'm the one adding the column). Also,
  40. * updated the producthistory template with a new class for that
  41. * column.
  42. * NOTE: I'll have to watch out for any updates/changes to the site
  43. * as I replace the whole function, since obviously I cannot
  44. * just patch the existing code. Oh, wait...
  45. * NODO: I know they say "eval() is evil()", but what if I'd say the
  46. * words "toString()", "String.replace()" and "eval()" in that
  47. * particular order... OK, I'll leave that hanging in the air.
  48. * - Took the opportunity to fix the call that sets the initial width
  49. * of the "Product History Management" dialog, which was failing with
  50. * errors of the sort "Permission denied to access property".
  51. * (2015-06-30)
  52. * 0.1.5 Change made:
  53. * - Added outofmilk.com as a possible domain for include URLs.
  54. * (2015-04-02)
  55. * 0.1.4 Changes made:
  56. * - Changed how the initial width of the "Product History Management"
  57. * dialog is set.
  58. * - Implemented changes to add a "UPC" column to the product history
  59. * table (by changing its jQuery UI dialog template; this is possible
  60. * since the web service's "GetAllProductHistoryItems" method already
  61. * returns the stored UPC value for each history item).
  62. * - Removed keep-alive code since no longer necessary.
  63. * (2013-08-19)
  64. * 0.1.3 Changes made:
  65. * - Removed "!important" when setting the (initial) width property of
  66. * the "Product History Management" dialog (since the specified width
  67. * isn't meant to be permanent).
  68. * (2013-04-05)
  69. * 0.1.2 Changes made:
  70. * - Added javascript code to keep the session alive (without the
  71. * need to refresh the page).
  72. * (2013-04-04)
  73. * 0.1.1 Changes made:
  74. * - Added column names for dialog "Product History Management".
  75. * - Changed text alignment for (newly-named) column "Tax Exempt".
  76. * (2013-04-04)
  77. * 0.1.0 First implementation. (2013-04-02)
  78. *
  79. */
  80.  
  81. (function() {
  82. // constants
  83. var USERSCRIPT_NAME = 'OutOfMilk.com Shopping List Enhancements';
  84.  
  85. /*
  86. * The Payload
  87. */
  88.  
  89. // css definitions
  90. var css_fixes =
  91. '@namespace url(http://www.w3.org/1999/xhtml);\n' +
  92. // Changes & Overrides
  93. // background overlays for modal dialog with fixed postion
  94. '.ui-widget-overlay { position: fixed /* original: absolute */ !important ; }\n' +
  95. // "Product History Management" dialog - increase initial width
  96. // '-> now done through javascript
  97. // //'div[aria-describedby="manageproducthistoryform"] { width: 80% /* original: 600px */ ; }\n' +
  98. // "Product History Management" dialog - take full parent's width for table within dialog
  99. 'table.producthistory-table { width: 100% /* original: 550px */ !important ; }\n' +
  100. // "Product History Management" dialog - column headers
  101. // 2015-09-17: Column headers not needed anymore (done through the template itself)
  102. ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(1) > strong:before { ' +
  103. /// 'content: "Item" /* original: (none specified) */ !important ; }\n' +
  104. ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(3) > strong:before { ' +
  105. /// 'content: "Tax Exempt" /* original: (none specified) */ !important ; }\n' +
  106. ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(4) > strong:before { ' +
  107. /// 'content: "Category" /* original: (none specified) */ !important ; }\n' +
  108. ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(5) > strong:before { ' +
  109. /// 'content: "UPC" /* original: (none specified) */ !important ; }\n' +
  110. 'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(6):before { ' +
  111. /// 'content: "Actions" /* original: (none specified) */ !important ; ' +
  112. 'text-align: center /* original: left (through inheritance) */ !important ; ' +
  113. '}\n' +
  114. 'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(5) { ' +
  115. 'text-align: center /* original: left (through inheritance) */ !important ; ' +
  116. '}\n' +
  117. // "Product History Management" dialog - values for column "Tax Exempt" centered horizontally
  118. 'td.producthistorytaxfree { text-align: center /* original: left (through inheritance) */ !important ; }\n' +
  119. // "Edit Product History" dialog - wider "Description" field
  120. '#ctl00_ctl00_ContentPlaceHolder1_EditProductHistoryDialog1_txtEditProductHistoryDescription { ' +
  121. 'width: 350px /* original: (none specified) */ !important ; }\n' +
  122. // <END>
  123. '';
  124. // new "producthistory-template" template
  125. var templateProductHistory = function() {
  126. /**HEREDOC
  127. <script type="producthistory-template">
  128. <# if(this.dataobjects.length > 0) { #>
  129. <tr>
  130. <th><strong>Description</strong></th>
  131. <th><strong>Price</strong></th>
  132. <th><strong>Tax Free?</strong></th>
  133. <th><strong>Category</strong></th>
  134. <th><strong>UPC</strong></th>
  135. <th colspan="3">Actions</th>
  136. </tr>
  137. <# $.each(this.dataobjects, function(i, object) { #>
  138. <tr>
  139. <td class="producthistoryid hidden">
  140. <#= object.ID #>
  141. </td>
  142. <td>
  143. <span class="producthistorydescription"><#= trimDescription(object.Description,60,"<acronym title=\"" + object.Description + "\">...</acronym>") #></span>
  144. </td>
  145. <td class="producthistoryprice">
  146. <span><#= FormatNumberCurrency(object.Price) #></span>
  147. </td>
  148. <td class="producthistorytaxfree">
  149. <span>
  150. <# if (object.TaxFree) { #>
  151. Yes
  152. <# } else { #>
  153. No
  154. <# } #>
  155. </span>
  156. </td>
  157. <td class="producthistorycategory">
  158. <span><#= object.CategoryName #></span>
  159. </td>
  160. <td class="producthistoryupc">
  161. <span><#= object.UPC #></span>
  162. </td>
  163. <td>
  164. <a href="javascript:void(0);" class="btn-default addproducthistory"><span>Add To List</span></a>
  165. </td>
  166. <td>
  167. <a href="javascript:void(0);" class="btn-default editproducthistory"><span>Edit</span></a>
  168. </td>
  169. <td class="last-column">
  170. <a href="javascript:void(0);" class="btn-default deleteproducthistory"><span>Delete</span></a>
  171. </td>
  172. </tr>
  173. <# }); #>
  174. <# } else { #>
  175. <tr>
  176. <td colspan="5">There are no items to display</td>
  177. </tr>
  178. <# } #>
  179. </script>
  180. HEREDOC**/
  181. };
  182. // new implementation of saveProductHistory()
  183. var mySaveProductHistory = function($this) {
  184. if(validateProductHistoryForm()){
  185. var ID = $(".editproducthistoryid").html();
  186. var description = $(".editproducthistorydescription").val();
  187. var price = $(".editproducthistoryprice").val();
  188. var taxfree = $(".editproducthistorytaxfree input").is(":checked");
  189. var upc = $(".editproducthistoryupc").val();
  190.  
  191. var Params = { "ID": ID, "description":description, "price": price, "upc":upc, "taxfree": taxfree };
  192. var jQueryParams = JSON.stringify(Params);
  193.  
  194. $.ajax({
  195. type: "POST",
  196. url: "Services/GenericService.asmx/UpdateProductHistoryItem",
  197. data: jQueryParams,
  198. contentType: "application/json; charset=utf-8",
  199. dataType: "json",
  200. success: function (msg) {
  201. if(msg.d == false){
  202. $(".producthistoryitemvalidation").html("An item already exists with this description and price!");
  203. } else {
  204. var $element = $(".producthistoryid:contains("+ID+")").parents("tr");
  205. $element.find(".producthistorydescription").html(description);
  206. $element.find(".producthistoryprice").html(FormatNumberCurrency(price));
  207. /* added --> */ $element.find(".producthistoryupc").html(upc);
  208. if(taxfree) {
  209. $element.find(".producthistorytaxfree").html("Yes");
  210. } else {
  211. $element.find(".producthistorytaxfree").html("No");
  212. }
  213.  
  214. $this.dialog("close");
  215. }
  216. },
  217. failure: function (msg) {
  218. }
  219. });
  220. }
  221. };
  222. // of course, I could also do something like:
  223. /*
  224. eval('var mySaveProductHistory = ' +
  225. unsafeWindow.saveProductHistory.toString().replace(/(html\(FormatNumberCurrency\(price\)\);)/, '$1\n$$element.find(".producthistoryupc").html(upc);')
  226. );
  227. */
  228. // but, would blindly patching code be actually better than
  229. // replacing a whole function? Yeah, I thought so.
  230.  
  231. // 2015-09-17: fix for the "Tax Free" checkbox not working in the "shoppingeditpopup" dialog
  232. var shoppingeditpopup_fix1 = function() {
  233. // find the "Tax Free" checkbox and its <div> container
  234. var $element = $(".shoppingeditpopup"),
  235. taxfree = $element.find("input#checkbox1"),
  236. taxfreeParentDiv = taxfree.parent();
  237. // add the missing "taxfree" class, such that in method
  238. // ShoppingEngine.updateShoppingItem() the line:
  239. // ---
  240. // var taxfree = $element.find(".taxfree input").is(":checked");
  241. // ---
  242. // works properly
  243. taxfreeParentDiv.addClass('taxfree');
  244. };
  245. // 2015-09-17: fix for the empty <select> element in the "shoppingeditpopup" dialog
  246. var shoppingeditpopup_fix2 = function() {
  247. // find the "How Much?" <select> element for units (removing any children in the process)
  248. var $element = $(".shoppingeditpopup"),
  249. editUnits = $element.find("select#drpUnitsEdit").find("option").remove().end(),
  250. itemUnitOptions = $(".shoppinglistitem").find("select.itemunit").find("option");
  251. // copy the options for the "shoppinglistitem" dialog
  252. $.each(itemUnitOptions, function() {
  253. editUnits.append($("<option />").val($(this).val()).text($(this).text()));
  254. });
  255. };
  256. // 2015-09-17: methods ShoppingEngine.addShoppingItem() & ShoppingEngine.updateShoppingItem()
  257. // don't use proper links for "icon-taxfree.png"; fix that
  258. var iconTaxFreeURLs_fix = function() {
  259. // patch using .toString() & eval()
  260. $.each(['addShoppingItem', 'updateShoppingItem'], function() {
  261. eval("ShoppingEngine." + this + " = " + ShoppingEngine[this].toString().replace("src='Images/icon-taxfree.png", "src='\"+ STATIC_URL +\"images/icon-taxfree.png'"));
  262. });
  263. };
  264.  
  265. /*
  266. * The Tools
  267. */
  268.  
  269. // heredoc parser
  270. var getHeredoc = function(container, identifier) {
  271. // **WARNING**: Inputs not filtered (e.g. types, illegal chars within regex, etc.)
  272. var re = new RegExp("/\\*\\*" + identifier + "[\\n\\r]+[\\s\\S]*?[\\n\\r]+" + identifier + "\\*\\*/", "m");
  273. var str = container.toString();
  274. str = re.exec(str).toString();
  275. str = str.replace(new RegExp("/\\*\\*" + identifier + "[\\n\\r]+",'m'),'').toString();
  276. return str.replace(new RegExp("[\\n\\r]+" +identifier + "\\*\\*/",'m'),'').toString();
  277. };
  278. // template substitution
  279. var replaceDialogTemplate = function(templateName, newContent) {
  280. var scripts = document.getElementsByTagName('script');
  281. if (scripts.length > 0) {
  282. for (var i = 0; i < scripts.length; i++) {
  283. if (scripts[i].getAttribute('type') == templateName) {
  284. // replace template content
  285. scripts[i].innerHTML = newContent.toString().replace(/^[\r\n\s]*<script[^>]*>|<\/script>[\r\n\s]*$/g, '');
  286. return;
  287. }
  288. }
  289. }
  290. };
  291. // code injection
  292. var injectCode = function(code){
  293. var tmpScript = document.createElement('script');
  294. tmpScript.id = '__iC_script-'+Math.random().toString().slice(2);
  295. tmpScript.type = 'text/javascript';
  296. tmpScript.textContent = (function() {
  297. return [
  298. ';('+(function () {
  299. /*code*/
  300. var thisScript = document.getElementById('/*scriptId*/');
  301. if (thisScript) { thisScript.parentNode.removeChild(thisScript); } // <-- oh no, you didn't!!
  302. }).toString()+')();',
  303. { k: 'code', v: (typeof(code) == 'string' && code.trim().length > 0 ? code : '/*failed*/') },
  304. { k: 'scriptId', v: tmpScript.id }
  305. ].reduce(function(base, mapping){
  306. return base.replace('/*'+mapping.k+'*/', mapping.v);
  307. });
  308. })();
  309. document.head.appendChild(tmpScript);
  310. };
  311. var injectFuncCode = function(func){
  312. if (typeof(func) !== 'function') return;
  313. injectCode('('+func.toString()+')();');
  314. };
  315. // code injection, specialized
  316. var replaceUnsafeFunc = function(targetName, newFuncImpl){
  317. // inspiration: https://greasyfork.org/en/scripts/2599-gm-2-port-function-override-helper/code
  318. var tmpScript = document.createElement('script');
  319. tmpScript.id = '__rUF_script-'+Math.random().toString().slice(2);
  320. tmpScript.type = 'text/javascript';
  321. tmpScript.textContent = (function() {
  322. return [
  323. ';('+(function () {
  324. window/*target*/ = /*newFunc*/window;
  325. var thisScript = document.getElementById('/*scriptId*/');
  326. if (thisScript) { thisScript.parentNode.removeChild(thisScript); } // <-- oh no, you didn't!!
  327. }).toString()+')();',
  328. { k: 'target', v: '.'+(typeof(targetName) == 'string' && targetName.trim().length > 0 ? targetName : '_void') },
  329. { k: 'newFunc', v: (typeof(newFuncImpl) == 'function' ? newFuncImpl : function(){}).toString()+';//' },
  330. { k: 'scriptId', v: tmpScript.id }
  331. ].reduce(function(base, mapping){
  332. return base.replace('/*'+mapping.k+'*/', mapping.v);
  333. });
  334. })();
  335. document.head.appendChild(tmpScript);
  336. };
  337. // reference some outside objects
  338. window.console = window.console || (function() {
  339. if (typeof(unsafeWindow) == 'undefined') return { 'log': function() {} };
  340. return unsafeWindow.console;
  341. })();
  342. // self-explanatory
  343. document.addStyle = function(css) {
  344. if (typeof(GM_addStyle) != 'undefined') {
  345. GM_addStyle(css);
  346. } else {
  347. var heads = this.getElementsByTagName('head');
  348. if (heads.length > 0) {
  349. var node = this.createElement('style');
  350. node.type = 'text/css';
  351. node.appendChild(this.createTextNode(css));
  352. heads[0].appendChild(node);
  353. }
  354. }
  355. };
  356.  
  357. /*
  358. * The Action
  359. */
  360.  
  361. // css injection
  362. document.addStyle(css_fixes);
  363. // javascript patching
  364. try {
  365. // replace template "producthistory-template"
  366. replaceDialogTemplate('producthistory-template', getHeredoc(templateProductHistory, 'HEREDOC'));
  367. // replace saveProductHistory()
  368. replaceUnsafeFunc('saveProductHistory', mySaveProductHistory);
  369. // set initial width of "Product History Management" dialog
  370. replaceUnsafeFunc('onload', function(){ $("#manageproducthistoryform").dialog("option", "width", "80%"); });
  371. // 2015-09-17: fixes for the "shoppingeditpopup" dialog
  372. injectFuncCode(shoppingeditpopup_fix1);
  373. injectFuncCode(shoppingeditpopup_fix2);
  374. // 2015-09-17: fixes for "icon-taxfree.png" URLs
  375. injectFuncCode(iconTaxFreeURLs_fix);
  376. } catch(err) {
  377. console.log(err);
  378. }
  379.  
  380. /*
  381. * The End
  382. */
  383.  
  384. console.log('User script "' + USERSCRIPT_NAME + '" has completed.');
  385. })();