OutOfMilk.com Shopping List Enhancements

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

当前为 2024-01-07 提交的版本,查看 最新版本

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