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