OutOfMilk.com Shopping List Enhancements

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

  1. // ==UserScript==
  2. // @name OutOfMilk.com Shopping List Enhancements
  3. // @version 0.2.9
  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. // @match https://outofmilk.com/shoppinglist.aspx*
  9. // @grant GM_addStyle
  10. // ==/UserScript==
  11.  
  12. /***
  13. * History:
  14. *
  15. * 0.2.9 Change made:
  16. * - Removed legacy URLs, including unsecured ones.
  17. * - Updated matched URL to "https://outofmilk.com/shoppinglist.aspx"
  18. * - Within the mySaveProductHistory() fix, defining $.growlUIStatic()
  19. * in case it isn't found when scoped back into unsecure content
  20. * (but mostly, so JSLint can shut up about it not being defined).
  21. * - Also in mySaveProductHistory(), just as with myAddShoppingItem(),
  22. * now using $.growlUIStatic() for showing dialogs just as by the
  23. * original code being replaced.
  24. * (2022-04-21)
  25. * unrel Changes made:
  26. * - Commented out fix implemented through shoppingeditpopup_fix1(), as
  27. * the related issue seems to have been fixed on the target page.
  28. * - Fixed proper linking to "icon-taxfree.png" images used by templates
  29. * by retrieving the relative path directly from them.
  30. * - Before injecting re-implemented handlers & fixing functions, now
  31. * making sure they exist.
  32. * (2020-07-23)
  33. * 0.2.8 Change made:
  34. * - For method mySaveProductHistory(), fixed how an item's properties
  35. * are parsed, namely where the wrong jQuery function was being used
  36. * to evaluate its value (e.g. {}.html() where it should have been
  37. * {}.text()) and for integer/float values left as strings (where
  38. * parseInt()/parseFloat() should have been used).
  39. * (2020-04-27)
  40. * 0.2.7 Changes made:
  41. * - Modified part where we reference the outside window.console object
  42. * so we don't redefine the log() function of window.console with an
  43. * empty function but instead define a local-scope console object.
  44. * - Now using @match instead of @include in the metadata block.
  45. * - For iconTaxFreeURLs_fix(), not using eval() anymore.
  46. * - Made code cleanups here and there.
  47. * (2019-10-09)
  48. * 0.2.6 Changes made:
  49. * - Improved declarations of tool methods injectCode(), injectFuncCode()
  50. * and replaceUnsafeFunc() to denote optional arguments supported for
  51. * each. Also, reworked their code a bit.
  52. * - For method replaceUnsafeFunc(), added support for two new optional
  53. * arguments: the first one to specify the parent object whose function
  54. * must be replaced, when it's not in the global scope (i.e. window);
  55. * and the second to specify a delay before which the function
  56. * replacement should be done, i.e. for objects or functions created
  57. * (or exposed) through deferred scripts.
  58. * - For methods injectCode() and injectFuncCode(), added as new optional
  59. * argument the possibility to specify an execution delay, just like
  60. * for replaceUnsafeFunc().
  61. * - Added the possibility to specify a UPC when adding a new item to the
  62. * shopping list. To do so, we add a new UPC field to the "New Item"
  63. * form and then replace that form's handling method with a version
  64. * supporting the new field.
  65. * Unfortunately, for the time being it seems like the web service
  66. * ignores any value used as the UPC (though to begin with the spec was
  67. * already there to show how it should be passed when making the call).
  68. * (2018-08-06)
  69. * 0.2.5 Changes made:
  70. * - Tweaked the UPC parsing code a bit.
  71. * - In the future, this script should be modified to allow users to
  72. * provide their own link templates for looking up products by their
  73. * stored UPC codes.
  74. * (2016-08-09) *not publicly released
  75. * 0.2.4 Change made:
  76. * - Completed the UPC parsing code, including expanding UPC-E barcodes,
  77. * as part of the product links building added in version 0.2.3.
  78. * (2016-06-21) *not publicly released
  79. * 0.2.3 Change made:
  80. * - On the "Product History Management" dialog, added a column for
  81. * links to the corresponding product on a (personally-used) grocery
  82. * store website (IGA.net).
  83. * (2016-06-02) *not publicly released
  84. * 0.2.2 Change made:
  85. * - For the fix around the "Tax Free" checkbox of the "Edit Product
  86. * History" dialog, a jQuery function call was failing so we're now
  87. * using its DOM native equivalent.
  88. * (2016-05-06)
  89. * 0.2.1 Changes made:
  90. * - Fixed the always-unchecked "Tax Free" checkbox for the dialog
  91. * "Edit Product History".
  92. * - Updated element id for the "Description" field of the dialog
  93. * "Edit Product History".
  94. * (2016-01-24)
  95. * 0.2.0 Changes made:
  96. * - Updated the "producthistory-template" template to match column
  97. * names recently added by outofmilk.com, while at the same time
  98. * disabling the CSS code previously used to display custom column
  99. * headers.
  100. * - Updated the "producthistory-template" template so that, like the
  101. * original one, "Yes" & "No" are used as values for the "Tax Free?"
  102. * column instead of "true" & "false".
  103. * - Fixed the non-working "Tax Free" checkbox and empty "How Much?"
  104. * dropdown list for dialog "shoppingeditpopup".
  105. * - Fixed the URL used for the "icon-taxfree.png" image whenever
  106. * adding or editing an item that is tax free.
  107. * (2015-09-17)
  108. * 0.1.7 Changes made:
  109. * - Updated script for use with the repository [greasyfork.org].
  110. * - No change made to the code.
  111. * (2015-09-14)
  112. * 0.1.6 Changes made:
  113. * - Kept being annoyed that everytime I added/changed a UPC from the
  114. * product history it wouldn't update, so now I re-implement method
  115. * saveProductHistory() (from "ProductManagement.js?v=...") with the
  116. * added tweaks (after all, I'm the one adding the column). Also,
  117. * updated the producthistory template with a new class for that
  118. * column.
  119. * NOTE: I'll have to watch out for any updates/changes to the site
  120. * as I replace the whole function, since obviously I cannot
  121. * just patch the existing code. Oh, wait...
  122. * NODO: I know they say "eval() is evil()", but what if I'd say the
  123. * words "toString()", "String.replace()" and "eval()" in that
  124. * particular order... OK, I'll leave that hanging in the air.
  125. * - Took the opportunity to fix the call that sets the initial width
  126. * of the "Product History Management" dialog, which was failing with
  127. * errors of the sort "Permission denied to access property".
  128. * (2015-06-30)
  129. * 0.1.5 Change made:
  130. * - Added outofmilk.com as a possible domain for include URLs.
  131. * (2015-04-02)
  132. * 0.1.4 Changes made:
  133. * - Changed how the initial width of the "Product History Management"
  134. * dialog is set.
  135. * - Implemented changes to add a "UPC" column to the product history
  136. * table (by changing its jQuery UI dialog template; this is possible
  137. * since the web service's "GetAllProductHistoryItems" method already
  138. * returns the stored UPC value for each history item).
  139. * - Removed keep-alive code since no longer necessary.
  140. * (2013-08-19)
  141. * 0.1.3 Changes made:
  142. * - Removed "!important" when setting the (initial) width property of
  143. * the "Product History Management" dialog (since the specified width
  144. * isn't meant to be permanent).
  145. * (2013-04-05)
  146. * 0.1.2 Changes made:
  147. * - Added javascript code to keep the session alive (without the
  148. * need to refresh the page).
  149. * (2013-04-04)
  150. * 0.1.1 Changes made:
  151. * - Added column names for dialog "Product History Management".
  152. * - Changed text alignment for (newly-named) column "Tax Exempt".
  153. * (2013-04-04)
  154. * 0.1.0 First implementation. (2013-04-02)
  155. *
  156. */
  157.  
  158. (function() {
  159. 'use strict';
  160.  
  161. // constants
  162. var USERSCRIPT_NAME = 'OutOfMilk.com Shopping List Enhancements';
  163.  
  164. /*
  165. * The Payload
  166. */
  167.  
  168. // css definitions
  169. var css_fixes =
  170. '@namespace url(http://www.w3.org/1999/xhtml);\n' +
  171. // Changes & Overrides
  172. // background overlays for modal dialog with fixed postion
  173. '.ui-widget-overlay { position: fixed /* original: absolute */ !important ; }\n' +
  174. // "Product History Management" dialog - increase initial width
  175. // '-> now done through javascript
  176. // //'div[aria-describedby="manageproducthistoryform"] { width: 80% /* original: 600px */ ; }\n' +
  177. // "Product History Management" dialog - take full parent's width for table within dialog
  178. 'table.producthistory-table { width: 100% /* original: 550px */ !important ; }\n' +
  179. // "Product History Management" dialog - column headers
  180. // 2015-09-17: Column headers not needed anymore (done through the template itself)
  181. ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(1) > strong:before { ' +
  182. /// 'content: "Item" /* original: (none specified) */ !important ; }\n' +
  183. ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(3) > strong:before { ' +
  184. /// 'content: "Tax Exempt" /* original: (none specified) */ !important ; }\n' +
  185. ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(4) > strong:before { ' +
  186. /// 'content: "Category" /* original: (none specified) */ !important ; }\n' +
  187. ///'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(5) > strong:before { ' +
  188. /// 'content: "UPC" /* original: (none specified) */ !important ; }\n' +
  189. 'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(6):before { ' +
  190. /// 'content: "Actions" /* original: (none specified) */ !important ; ' +
  191. 'text-align: center /* original: left (through inheritance) */ !important ; ' +
  192. '}\n' +
  193. 'table#producthistorytable.table-default > tr:nth-child(1) > th:nth-child(5) { ' +
  194. 'text-align: center /* original: left (through inheritance) */ !important ; ' +
  195. '}\n' +
  196. // "Product History Management" dialog - values for column "Tax Exempt" centered horizontally
  197. 'td.producthistorytaxfree { text-align: center /* original: left (through inheritance) */ !important ; }\n' +
  198. // "Edit Product History" dialog - wider "Description" field
  199. '#txtEditProductHistoryDescription { ' +
  200. 'width: 380px /* original: (none specified) */ !important ; }\n' +
  201. // <END>
  202. '';
  203.  
  204. // new "producthistory-template" template
  205. var templateProductHistory = function() {
  206. /**HEREDOC
  207. <script type="producthistory-template">
  208. <##
  209. if (!String.prototype.reverse) {
  210. String.prototype.reverse = function() {
  211. return this.split('').reverse().join('');
  212. };
  213. }
  214. var eanCheckDigit = function(s) {
  215. var result = 0;
  216. var rs = s.reverse();
  217. for (counter = 0; counter < rs.length; counter++) {
  218. result = result + parseInt(rs.charAt(counter)) * Math.pow(3, ((counter+1) % 2));
  219. }
  220. return (10 - (result % 10)) % 10;
  221. };
  222. ##>
  223. <# if(this.dataobjects.length > 0) { #>
  224. <tr>
  225. <th><strong>Description</strong></th>
  226. <th><strong>Price</strong></th>
  227. <th><strong>Tax Free?</strong></th>
  228. <th><strong>Category</strong></th>
  229. <th><strong>UPC</strong></th>
  230. <th><strong>Links</strong></th>
  231. <th colspan="3">Actions</th>
  232. </tr>
  233. <# $.each(this.dataobjects, function(i, object) { #>
  234. <tr>
  235. <td class="producthistoryid hidden"><#= object.ID #></td>
  236. <td>
  237. <span class="producthistorydescription"><#= trimDescription(object.Description,60,"<acronym title=\"" + object.Description + "\">...</acronym>") #></span>
  238. </td>
  239. <td class="producthistoryprice">
  240. <span><#= FormatNumberCurrency(object.Price) #></span>
  241. </td>
  242. <td class="producthistorytaxfree">
  243. <span>
  244. <# if (object.TaxFree) { #>
  245. Yes
  246. <# } else { #>
  247. No
  248. <# } #>
  249. </span>
  250. </td>
  251. <td class="producthistorycategory">
  252. <span><#= object.CategoryName #></span>
  253. </td>
  254. <td class="producthistoryupc">
  255. <span><#= object.UPC #></span>
  256. <##
  257. object.UPC = String(object.UPC);
  258. if (object.UPC.length >= 12) {
  259. object.UPC12 = object.UPC.substr(-12,12);
  260. if (object.UPC12.substr(0,2) === '00') {
  261. object.UPC12 = object.UPC12.substr(1,11);
  262. object.UPC12 += eanCheckDigit(object.UPC12);
  263. }
  264. } else if (object.UPC.length === 6 && object.UPC.substr(0,1) === "F") {
  265. object.UPC12 = '000000' + object.UPC.substr(1,5);
  266. object.UPC12 += eanCheckDigit(object.UPC12);
  267. } else if (object.UPC.length === 8) {
  268. object.UPC12 = object.UPC.charAt(0);
  269. switch (object.UPC.charAt(6)) {
  270. case '0':
  271. case '1':
  272. case '2':
  273. object.UPC12 += object.UPC.substr(1, 2) + object.UPC.substr(6, 1) + '0000' + object.UPC.substr(3, 3) + object.UPC.substr(7, 1);
  274. break;
  275. case '3':
  276. object.UPC12 += object.UPC.substr(1, 3) + '00000' + object.UPC.substr(4, 2) + object.UPC.substr(7, 1);
  277. break;
  278. case '4':
  279. object.UPC12 += object.UPC.substr(1, 4) + '00000' + object.UPC.substr(5, 1) + object.UPC.substr(7, 1);
  280. break;
  281. case '5':
  282. case '6':
  283. case '7':
  284. case '8':
  285. case '9':
  286. object.UPC12 += object.UPC.substr(1, 5) + '0000' + object.UPC.substr(6, 1) + object.UPC.substr(7, 1);
  287. break;
  288. default: object.UPC12 = '';
  289. }
  290. } else {
  291. object.UPC12 = '';
  292. }
  293. ##>
  294. </td>
  295. <td class="producthistorylink">
  296. <span><# if (object.UPC12.length > 0) { #>
  297. <a target="_blank" href="https://www.iga.net/en/search?k=00<#= object.UPC12.substr(0, 11) #>"><strong>IGA</strong></a>
  298. <# } else { #>
  299. &nbsp;
  300. <# } #></span>
  301. </td>
  302. <td>
  303. <a href="javascript:void(0);" class="btn-default addproducthistory"><span>Add To List</span></a>
  304. </td>
  305. <td>
  306. <a href="javascript:void(0);" class="btn-default editproducthistory"><span>Edit</span></a>
  307. </td>
  308. <td class="last-column">
  309. <a href="javascript:void(0);" class="btn-default deleteproducthistory"><span>Delete</span></a>
  310. </td>
  311. </tr>
  312. <# }); #>
  313. <# } else { #>
  314. <tr>
  315. <td colspan="5">There are no items to display</td>
  316. </tr>
  317. <# } #>
  318. </script>
  319. HEREDOC**/
  320. };
  321.  
  322. var validateProductHistoryForm = validateProductHistoryForm || window.validateProductHistoryForm || function() {},
  323. $ = $ || window.$ || function() {},
  324. FormatNumberCurrency = FormatNumberCurrency || window.FormatNumberCurrency || function() {},
  325. ShoppingEngine = ShoppingEngine || window.ShoppingEngine || { validateShoppingItemForm: function() {} },
  326. usedAutoComplete = usedAutoComplete || window.usedAutoComplete || false,
  327. ListID = ListID || window.ListID || {},
  328. gettext = gettext || window.gettext || {},
  329. globalRes = globalRes || window.globalRes || { TaxFree: false },
  330. STATIC_URL = STATIC_URL || window.STATIC_URL || "",
  331. FormatNumber = FormatNumber || window.FormatNumber || function() {},
  332. ListEngine = ListEngine || window.ListEngine || { bindListItemsEdit: function() {}, bindContextMenus: function() {} },
  333. sortLists = sortLists || window.sortLists || function() {},
  334. Apprise = Apprise || window.Apprise || function() {},
  335. jQuery = jQuery || window.jQuery || function() {};
  336.  
  337. // new implementation of saveProductHistory()
  338. var mySaveProductHistory = function($this) {
  339. // new implementation:
  340. if(validateProductHistoryForm()){
  341. var ID = parseInt($(".editproducthistoryid").text(), 10);
  342. var description = $(".editproducthistorydescription").val();
  343. var price = parseFloat($(".editproducthistoryprice").val());
  344. var taxfree = $(".editproducthistorytaxfree input").is(":checked");
  345. var upc = $(".editproducthistoryupc").val();
  346.  
  347. var Params = { "ID": ID, "description": description, "price": price, "upc": upc, "taxfree": taxfree };
  348. var jQueryParams = JSON.stringify(Params);
  349.  
  350. $.growlUIStatic = (typeof $.growlUIStatic === 'function')
  351. ? $.growlUIStatic
  352. : function(title, message, onClose) {
  353. var $m = $('<div class="growlUI"></div>');
  354. if (title) $m.append('<h1>'+title+'</h1>');
  355. if (message) $m.append('<h2>'+message+'</h2>');
  356. $.blockUI({
  357. message: $m, fadeIn: 700, fadeOut: 1000, centerY: false,
  358. timeout: 0, showOverlay: false,
  359. onUnblock: onClose,
  360. css: $.blockUI.defaults.growlCSS
  361. });
  362. };
  363. $.growlUIStatic(gettext('Please wait...'), gettext('Saving changes to product history item...'));
  364.  
  365. $.ajax({
  366. type: "POST",
  367. url: "Services/GenericService.asmx/UpdateProductHistoryItem",
  368. data: jQueryParams,
  369. contentType: "application/json; charset=utf-8",
  370. dataType: "json",
  371. success: function (msg) {
  372. $.unblockUI();
  373. if(msg.d === false){
  374. $(".producthistoryitemvalidation").html(gettext("An item already exists with this description and price!"));
  375. } else {
  376. var $element = $(".producthistoryid:contains("+ID+")").parents("tr");
  377. $element.find(".producthistorydescription").html(description);
  378. $element.find(".producthistoryprice").html(FormatNumberCurrency(price));
  379. /* added --> */ $element.find(".producthistoryupc").html(upc);
  380. if(taxfree) {
  381. $element.find(".producthistorytaxfree").html("Yes");
  382. } else {
  383. $element.find(".producthistorytaxfree").html("No");
  384. }
  385. $.growlUI(gettext('Success!'), gettext('Changes to product history item saved'));
  386. $this.dialog("close");
  387. }
  388. },
  389. failure: function (data) {
  390. $.unblockUI();
  391. $.growlUI(gettext('Warning'), gettext('Deleting product history item failed!'));
  392. }
  393. });
  394. }
  395. };
  396. // of course, I could also do something like:
  397. /*
  398. eval('var mySaveProductHistory = ' +
  399. unsafeWindow.saveProductHistory.toString().replace(/(html\(FormatNumberCurrency\(price\)\);)/, '$1\n$$element.find(".producthistoryupc").html(upc);')
  400. );
  401. */
  402. // but, would blindly patching code be actually better than
  403. // replacing a whole function? Yeah, I thought so.
  404.  
  405. // 2018-08-06: new implementation of ShoppingEngine.addShoppingItem() to add a UPC field
  406. // NOTE: Not my code; I only added not even half a line
  407. var myAddShoppingItem = function() {
  408. // new implementation:
  409. if (ShoppingEngine.validateShoppingItemForm($(".shoppinglistitem"), false) != false) {
  410. var $element = $(".shoppinglistitem");
  411.  
  412. var description = $element.find(".itemdescription").val();
  413. var quantity = $element.find(".itemquantity").val();
  414. var unit = $element.find(".itemunit").val();
  415. var unittext = $element.find(".itemunit option:selected").text();
  416. var price = $element.find(".itemprice").val();
  417. var note = $element.find(".itemnote").val();
  418. var taxfree = $element.find(".taxfree input").is(":checked");
  419. var category = $element.find(".itemcategory").val();
  420. // 2018-08-06: Added a UPC field
  421. var UPC = $element.find(".itemupc").val() || '';
  422.  
  423. if (usedAutoComplete == undefined) { usedAutoComplete = false; }
  424.  
  425. var Params = { "ID": ListID, "usedAutoComplete": usedAutoComplete, "description": description, "quantity": quantity, "unit": unit, "price": price, "note": note, "taxfree": taxfree, "upc": UPC, "category": category, "promoproviderpromotionid": "", "promoproviderstaticid": 0, "useMinOrdinal": false, "isEmailPromo": false };
  426. var jQueryParams = JSON.stringify(Params);
  427.  
  428. $.growlUI(gettext('Please Wait...'), gettext('Adding your item to the list!'));
  429.  
  430. $.ajax({
  431. type: "POST",
  432. url: "Services/GenericService.asmx/AddShoppingItem",
  433. data: jQueryParams,
  434. contentType: "application/json; charset=utf-8",
  435. dataType: "json",
  436. success: function (msg) {
  437. var Data = JSON.parse(msg.d);
  438.  
  439. var $template = $(".itemtemplate").clone();
  440.  
  441. var flagsText = "";
  442. if (taxfree || note.length > 0) {
  443. flagsText = "<div class='additionalitems clearfix'>";
  444. if (taxfree) {
  445. flagsText = flagsText + "&nbsp; <acronym title='" + globalRes.TaxFree + "'><img src='"+ STATIC_URL +"images/icon-taxfree.png' /></acronym>";
  446. }
  447. if (note.length > 0) {
  448. flagsText = flagsText + "&nbsp; <acronym title=\"" + note.replace(/\"/g, '\'') + "\"><img src='"+ STATIC_URL +"images/icon-note.png' /></acronym>";
  449. }
  450. flagsText = flagsText + "</div>";
  451. }
  452.  
  453. $template.find(".cell-title").html(flagsText).text(description);
  454. $template.find(".cell-qty").html(FormatNumber(quantity) + " " + unittext);
  455. $template.find(".cell-price").html(FormatNumberCurrency(Data.Price));
  456. $template.find(".productid").html(Data.ID);
  457. $template.find(".itemguid").html(Data.GUID);
  458. $template.find(".createddate").html(Data.Created);
  459. $template.removeClass("hidden");
  460. $template.removeClass("itemtemplate");
  461. $template.attr("id", "item_" + Data.ID);
  462.  
  463. //Find all categories on the list and then insert before if we have a category, and insert after the beginning if we don't.
  464.  
  465. //if we have a category but it's not actually on the list, that means it was a newly created category list from the add item
  466. //so we will check for that and do a full list refresh if that's the case
  467. var foundCategory = false;
  468. if (Data.Category) {
  469. $(".category").each(function () {
  470. var catID = $(this).attr("id").split("cat_")[1];
  471. if (Data.Category == catID) {
  472. $template.insertAfter($(this));
  473. foundCategory = true;
  474. }
  475. })
  476. } else {
  477. //if there was no category set this to true so that we dont have a full refresh
  478. foundCategory = true;
  479. $template.insertAfter("#table_totals");
  480. }
  481.  
  482. $("#sortable tbody").sortable('refresh', { items: 'tr:not(.tabletop):not(.itemtemplate)' });
  483. ListEngine.bindListItemsEdit();
  484. ShoppingEngine.updateShoppingListPrice();
  485. ListEngine.bindContextMenus();
  486.  
  487. sortLists(true);
  488.  
  489. usedAutoComplete = false;
  490.  
  491. if (!foundCategory) {
  492. $("#btn-refresh-list").click();
  493. }
  494.  
  495. //Bind the click event for toggling an items status
  496. ListEngine.toggleItemStatus();
  497. },
  498. error: function (msg) {
  499. Apprise(gettext("Failed to insert item. Please try again.") + "<br /><br />" + gettext("If this problem persists, please contact us at") + " <a href='https://outofmilk.zendesk.com/hc/en-us/requests/new'>Support Request</a>", { 'confirm': true });
  500. }
  501. });
  502.  
  503. return true;
  504. } else {
  505. return false;
  506. }
  507. };
  508.  
  509. // 2015-09-17: fix for the "Tax Free" checkbox not working in the "shoppingeditpopup" dialog
  510. /*
  511. var shoppingeditpopup_fix1 = function() {
  512. // find the "Tax Free" checkbox and its <div> container
  513. var $element = $(".shoppingeditpopup"),
  514. taxfree = $element.find("input#checkbox1"),
  515. taxfreeParentDiv = taxfree.parent();
  516. // add the missing "taxfree" class, such that in method
  517. // ShoppingEngine.updateShoppingItem() the line:
  518. // ---
  519. // var taxfree = $element.find(".taxfree input").is(":checked");
  520. // ---
  521. // works properly
  522. taxfreeParentDiv.addClass('taxfree');
  523. };
  524. */
  525. // 2015-09-17: fix for the empty <select> element in the "shoppingeditpopup" dialog
  526. var shoppingeditpopup_fix2 = function() {
  527. // find the "How Much?" <select> element for units (removing any children in the process)
  528. var $element = $(".shoppingeditpopup"),
  529. editUnits = $element.find("select#drpUnitsEdit").find("option").remove().end(),
  530. itemUnitOptions = $(".shoppinglistitem").find("select.itemunit").find("option");
  531. // copy the options for the "shoppinglistitem" dialog
  532. $.each(itemUnitOptions, function() {
  533. editUnits.append($("<option />").val($(this).val()).text($(this).text()));
  534. });
  535. };
  536.  
  537. // 2015-09-17: methods ShoppingEngine.addShoppingItem() & ShoppingEngine.updateShoppingItem()
  538. // don't use proper links for "icon-taxfree.png"; fix that
  539. var iconTaxFreeURLs_fix = function() {
  540. // 2020-07-23: now retrieving link to "icon-taxfree.png" directly from template
  541. var templateSrc = $("script[type='product-template']"),
  542. toReplaceSrc = "src='../static/images/icon-taxfree.png'",
  543. taxfreeIconRE = /<# *if\(object\.TaxFree\) *\{ *#>\s+((?:.(?!<#))+)\s*<# *\} *#>/m,
  544. taxFreeIconResult = taxfreeIconRE.exec(templateSrc.html()),
  545. taxFreeIconHTML = (typeof taxFreeIconResult[1] !== 'undefined' ? taxFreeIconResult[1] : ''),
  546. taxFreeIconSrc = '';
  547. if (taxFreeIconHTML) {
  548. try {
  549. taxFreeIconSrc = /src="([^"]+\/icon-taxfree.png)"/.exec(taxFreeIconHTML)[1];
  550. console.log(taxFreeIconSrc);
  551. } catch(e) { /* ignore */ }
  552. }
  553. $.each(['updateShoppingItem'], function() {
  554. (new Function("this.ShoppingEngine." + this + " = " +
  555. ShoppingEngine[this].toString().replace(
  556. toReplaceSrc,
  557. (taxFreeIconSrc ? "src='" + taxFreeIconSrc + "'" : "src='\"+ STATIC_URL +\"images/icon-taxfree.png'")
  558. ))
  559. ).call(window);
  560. });
  561. };
  562.  
  563. // 2016-01-24: fix for the missing "producthistorytaxfree" class in the "editproducthistoryform" dialog
  564. var editproducthistory_fix1 = function() {
  565. // find the "Tax Free" <input type="checkbox"> element and its <td> parent
  566. var taxfree = $("#txtEditProductHistorytaxfree"),
  567. taxfreeParentTD = taxfree.parent();
  568. // add the missing "producthistorytaxfree" class, so that the lines
  569. // $(".editproducthistorytaxfree input").attr("checked", true);
  570. // $(".editproducthistorytaxfree input").attr("checked", false);
  571. // work as intended
  572. taxfreeParentTD.addClass('editproducthistorytaxfree');
  573. };
  574.  
  575. // 2016-01-24: fix for the "Tax Free" checkbox being always unchecked in the "editproducthistoryform" dialog
  576. var editproducthistory_fix2 = function() {
  577. // hook the "Manage Product History" button/link at right
  578. $(".manageproducts").on("click", function() {
  579. $(".editproducthistory").off();
  580. $(".editproducthistory").on("click", function() {
  581. var $parent = $(this).parents("tr");
  582. var ID = $parent.find(".producthistoryid").html();
  583. var description = $parent.find(".producthistorydescription").html();
  584. var price = $parent.find(".producthistoryprice").html();
  585. var Params = { "ID": ID };
  586. var jQueryParams = JSON.stringify(Params);
  587. $.growlUI('<img src="' + STATIC_URL + 'images/monster-help.png" />Please Wait...', 'Loading Product History item...');
  588. $.ajax({
  589. type: "POST",
  590. url: "Services/GenericService.asmx/GetProductHistoryItem",
  591. data: jQueryParams,
  592. contentType: "application/json; charset=utf-8",
  593. dataType: "json",
  594. success: function (msg) {
  595. $(".editproducthistorydescription").val(msg.d.Description);
  596. $(".editproducthistoryprice").val(FormatNumber(msg.d.Price));
  597. $(".editproducthistoryid").html(msg.d.ID);
  598. $(".editproducthistoryupc").val(msg.d.UPC);
  599. $(".editproducthistorytaxfree input").removeAttribute("checked");
  600. if (msg.d.TaxFree) {
  601. $(".editproducthistorytaxfree input").checked = true;
  602. } else {
  603. $(".editproducthistorytaxfree input").checked = false;
  604. }
  605. $("#editproducthistoryform").find('input').keypress(function (e) {
  606. if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
  607. $(".ui-dialog[aria-labelledby='ui-dialog-title-editproducthistoryform']").find('.ui-dialog-buttonpane').find('button:first').click();
  608. return false;
  609. }
  610. });
  611. $("#editproducthistoryform").dialog("open");
  612. },
  613. failure: function (msg) {
  614. }
  615. });
  616. });
  617. });
  618. };
  619.  
  620. // 2018-08-06: fix for adding a UPC field to the "New Item" form
  621. var listadditemform_fix = function() {
  622. // get form
  623. var $element = jQuery('.shoppinglistitem').first();
  624. // make sure we don't already have the UPC field present
  625. if ($element.length > 0 && $element.find('.itemupc').length == 0) {
  626. var itemPriceElem = $element.find('.itemprice').first();
  627. if (!itemPriceElem) { return; }
  628. itemPriceElem.parent().after('<strong><span>UPC (if any)</span><input type="text" value="" class="textbox itemupc"></strong>');
  629. }
  630. };
  631.  
  632. /*
  633. * The Tools
  634. */
  635.  
  636. // heredoc parser
  637. var getHeredoc = function(container, identifier) {
  638. // **WARNING**: Inputs not filtered (e.g. types, illegal chars within regex, etc.)
  639. var re = new RegExp("/\\*\\*" + identifier + "[\\n\\r]+[\\s\\S]*?[\\n\\r]+" + identifier + "\\*\\*/", "m");
  640. var str = container.toString();
  641. str = re.exec(str).toString();
  642. str = str.replace(new RegExp("/\\*\\*" + identifier + "[\\n\\r]+",'m'),'').toString();
  643. return str.replace(new RegExp("[\\n\\r]+" +identifier + "\\*\\*/",'m'),'').toString();
  644. };
  645.  
  646. // template substitution
  647. var replaceDialogTemplate = function(templateName, newContent) {
  648. var scripts = document.getElementsByTagName('script');
  649. if (scripts.length > 0) {
  650. for (var i = 0; i < scripts.length; i++) {
  651. if (scripts[i].getAttribute('type') == templateName) {
  652. var newText = newContent.toString();
  653. // remove comments
  654. newText = newText.replace(/\/\*(?:\r\n|\r|\n|.)+?\*\//gm, '').replace(/\/\/.+$/g, '');
  655. // remove empty lines
  656. newText = newText.replace(/$\s*\r\n/g, '');
  657. // make sure switch() blocks have their first case statement on the same line
  658. newText = newText.replace(/switch\s*\([^\{]+\)(\s*)\{(\s*)case/gm, function(match, p1, p2) {
  659. return match.replace(p1, ' ').replace(p2, ' ');
  660. });
  661. // process custom <## [..] ##> blocks
  662. newText = newText.replace(/\<##\s*((?:\r\n|\r|\n|.)+?)\s*##\>/gm, function(match, p) {
  663. return p.split(/\r\n|\r|\n/g).map(function(item, idx) {
  664. return item.replace(/^(\s*)(.*)/g, function(match, p1, p2) {
  665. return p1 + '<# ' + p2.trim() + ' #>';
  666. });
  667. }).join("\r\n");
  668. });
  669. // replace template content
  670. newText = newText.replace(/^[\r\n\s]*<script[^>]*>|<\/script>[\r\n\s]*$/g, '');
  671. scripts[i].innerHTML = newText;
  672. return;
  673. }
  674. }
  675. }
  676. };
  677.  
  678. // code injection
  679. var injectCode = function(code /* , idUniq = '', execDelay = 0 */){
  680. // get call arguments
  681. var idUniq = '',
  682. execDelay = 0,
  683. tmpScript = document.createElement('script');
  684. tmpScript.id = '__iC_script-'+Math.random().toString().slice(2);
  685. // argument #2 (optional): <script> element ID suffix
  686. if (arguments.length > 1) {
  687. idUniq = '_' + /[$_a-zA-Z][$_a-zA-Z0-9]*/.exec(arguments[1])[0] || '';
  688. }
  689. tmpScript.id = tmpScript.id + idUniq;
  690. // argument #3 (optional): execution delay
  691. if (arguments.length > 2) {
  692. execDelay = (arguments[2] === 'domready' ? arguments[2] : (parseInt(arguments[2]) || 0));
  693. }
  694. tmpScript.type = 'text/javascript';
  695. tmpScript.textContent = (function() {
  696. return [
  697. ';'+(
  698. execDelay === 'domready' ? '$(document).ready(' :
  699. (0+execDelay > 0 ? 'window.setTimeout(' : '(')
  700. )+(function () {
  701. /*code*/
  702. var thisScript = document.getElementById('/*scriptId*/');
  703. if (thisScript) { thisScript.parentNode.removeChild(thisScript); } // <-- oh no, you didn't!!
  704. }).toString()+(
  705. execDelay === 'domready' ? ')' :
  706. (0+execDelay > 0 ? ', '+execDelay+')' : ')()')
  707. )+';',
  708. { k: 'code', v: (typeof(code) == 'string' && code.trim().length > 0 ? code : '/*failed*/') },
  709. { k: 'scriptId', v: tmpScript.id }
  710. ].reduce(function(base, mapping){
  711. return base.replace('/*'+mapping.k+'*/', mapping.v);
  712. });
  713. })();
  714. // inject temporary script
  715. document.head.appendChild(tmpScript);
  716. };
  717. var injectFuncCode = function(func /* , idUniq = '', execDelay = 0 */){
  718. // make sure we got passed a function
  719. if (typeof(func) !== 'function') return;
  720. // get call arguments
  721. var idUniq = (arguments.length > 1 ? arguments[1] : ''),
  722. execDelay = (arguments.length > 2 ? (arguments[2] === 'domready' ? arguments[2] : (parseInt(arguments[2]) || 0)) : 0),
  723. wrapper = '('+func.toString()+')();';
  724. // inject temporary script
  725. injectCode(wrapper, idUniq, execDelay);
  726. };
  727.  
  728. // code injection, specialized
  729. // inspiration: https://greasyfork.org/en/scripts/2599-gm-2-port-function-override-helper/code
  730. var replaceUnsafeFunc = function(targetName, newFuncImpl /* , idUniq = '', thisScope = 'window', execDelay = 0 */){
  731. // get call arguments
  732. var idUniq = '',
  733. thisScope = 'window',
  734. execDelay = 0,
  735. tmpScript = document.createElement('script');
  736. tmpScript.id = '__rUF_script-'+Math.random().toString().slice(2);
  737. // argument #3 (optional): <script> element ID suffix
  738. if (arguments.length > 2) {
  739. idUniq = '_' + /[$_a-zA-Z][$_a-zA-Z0-9]*/.exec(arguments[2])[0] || '';
  740. }
  741. tmpScript.id = tmpScript.id + idUniq;
  742. // argument #4 (optional): scope of function to replace; by default, window (global)
  743. if (arguments.length > 3) {
  744. thisScope = ''+arguments[3].replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '') || 'window';
  745. }
  746. // argument #5 (optional): execution delay
  747. if (arguments.length > 4) {
  748. execDelay = (arguments[4] === 'domready' ? arguments[4] : (parseInt(arguments[4]) || 0));
  749. }
  750. tmpScript.type = 'text/javascript';
  751. tmpScript.textContent = (function() {
  752. return [
  753. ';'+(
  754. execDelay === 'domready' ? '$(document).ready(' :
  755. (0+execDelay > 0 ? 'window.setTimeout(' : '(')
  756. )+(function () {
  757. try {
  758. window/*scope*/ /*target*/ = /*newFunc*/window;
  759. } catch(_err) {
  760. console.log('Error in replaceUnsafeFunc() payload with id "/*scriptId*/": ' + _err.message);
  761. }
  762. var thisScript = document.getElementById('/*scriptId*/');
  763. if (thisScript) { thisScript.parentNode.removeChild(thisScript); } // <-- oh no, you didn't!!
  764. }).toString()+(
  765. execDelay === 'domready' ? ')' :
  766. (0+execDelay > 0 ? ', '+execDelay+')' : ')()')
  767. )+';',
  768. { k: 'scope', v: '.'+thisScope },
  769. { k: 'target', v: '.'+(typeof(targetName) == 'string' && targetName.trim().length > 0 ? targetName : '_void') },
  770. { k: 'newFunc', v: (typeof(newFuncImpl) == 'function' ? newFuncImpl : function(){}).toString()+';//' },
  771. { k: 'scriptId', v: tmpScript.id }
  772. ].reduce(function(base, mapping){
  773. // 2018-08-06: replaced [String].replace() with [String].split().join()
  774. ///return base.replace('/*'+mapping.k+'*/', mapping.v);
  775. return base.split('/*'+mapping.k+'*/').join(''+mapping.v);
  776. });
  777. })();
  778. // inject temporary script
  779. document.head.appendChild(tmpScript);
  780. };
  781.  
  782. // reference some outside objects
  783. var console = window.console || (function() {
  784. if (typeof(unsafeWindow) == 'undefined') return { 'log': function() {} };
  785. return unsafeWindow.console;
  786. })();
  787.  
  788. // self-explanatory
  789. document.addStyle = function(css /*, media */) {
  790. var media = (arguments.length > 1 ? arguments[1] : false);
  791. if (typeof(GM_addStyle) != 'undefined' && !media) {
  792. GM_addStyle(css);
  793. return true;
  794. } else {
  795. if (!media) { media = 'all'; }
  796. var heads = this.getElementsByTagName('head');
  797. if (heads.length > 0) {
  798. var node = this.createElement('style');
  799. node.type = 'text/css';
  800. if (media) node.media = media;
  801. if (node.appendChild(this.createTextNode(css))) {
  802. return (typeof heads[0].appendChild(node) != 'undefined');
  803. }
  804. }
  805. return false;
  806. }
  807. };
  808.  
  809. /*
  810. * The Action
  811. */
  812.  
  813. // css injection
  814. document.addStyle(css_fixes);
  815.  
  816. // javascript patching
  817. try {
  818. // replace template "producthistory-template"
  819. if (templateProductHistory) {
  820. replaceDialogTemplate('producthistory-template', getHeredoc(templateProductHistory, 'HEREDOC'));
  821. }
  822.  
  823. // replace saveProductHistory()
  824. if (typeof mySaveProductHistory === 'function') {
  825. replaceUnsafeFunc('saveProductHistory', mySaveProductHistory, 'mySaveProductHistory');
  826. }
  827.  
  828. // 2018-08-06: replace ShoppingEngine.addShoppingItem()
  829. if (typeof myAddShoppingItem === 'function') {
  830. replaceUnsafeFunc('addShoppingItem', myAddShoppingItem, 'myAddShoppingItem', 'ShoppingEngine', 'domready');
  831. }
  832.  
  833. // 2018-08-06: add the UPC text field to the "New Item" form
  834. if (typeof listadditemform_fix === 'function') {
  835. injectFuncCode(listadditemform_fix, 'listadditemform_fix');
  836. }
  837.  
  838. // set initial width of "Product History Management" dialog
  839. ///replaceUnsafeFunc('onload', function(){ $("#manageproducthistoryform").dialog("option", "width", "80%"); }, 'onload');
  840. // 2018-08-06: now making use of new features added to injectCode() and injectFuncCode()
  841. injectFuncCode(function(){ $("#manageproducthistoryform").dialog("option", "width", "80%"); }, 'manageproducthistoryform_fix', 'domready');
  842.  
  843. // 2015-09-17: fixes for the "shoppingeditpopup" dialog
  844. if (typeof shoppingeditpopup_fix1 === 'function') {
  845. injectFuncCode(shoppingeditpopup_fix1, 'shoppingeditpopup_fix1');
  846. }
  847. if (typeof shoppingeditpopup_fix2 === 'function') {
  848. injectFuncCode(shoppingeditpopup_fix2, 'shoppingeditpopup_fix2');
  849. }
  850.  
  851. // 2016-01-24: fixes for the "editproducthistoryform" dialog
  852. if (typeof editproducthistory_fix1 === 'function') {
  853. injectFuncCode(editproducthistory_fix1, 'editproducthistory_fix1');
  854. }
  855. if (typeof editproducthistory_fix2 === 'function') {
  856. injectFuncCode(editproducthistory_fix2, 'editproducthistory_fix2');
  857. }
  858.  
  859. // 2015-09-17: fixes for "icon-taxfree.png" URLs
  860. if (typeof iconTaxFreeURLs_fix === 'function') {
  861. injectFuncCode(iconTaxFreeURLs_fix, 'iconTaxFreeURLs_fix');
  862. }
  863. } catch(err) {
  864. console.log(err);
  865. }
  866.  
  867. /*
  868. * The End
  869. */
  870.  
  871. console.log('User script "' + USERSCRIPT_NAME + '" has completed.');
  872. })();