OutOfMilk.com Shopping List Enhancements

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

当前为 2016-05-06 提交的版本,查看 最新版本

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