Mint.com Customize Default Categories

Hide specified default built-in mint.com categories

  1. // ==UserScript==
  2. // @name Mint.com Customize Default Categories
  3. // @namespace com.schrauger.mint.js
  4. // @author Stephen Schrauger
  5. // @description Hide specified default built-in mint.com categories
  6. // @homepage https://github.com/schrauger/mint.com-customize-default-categories
  7. // @include https://*.mint.com/*
  8. // @include https://mint.intuit.com/*
  9. // @version 1.4.5
  10. // @grant none
  11. // ==/UserScript==
  12. /*jslint browser: true*/
  13. /*global jQuery*/
  14. (function () {
  15. function after_jquery() {
  16. var number_of_bit_arrays = 3; // use 3 arrays to store all preferences.
  17. var unique_id_length = 4; // the bit array is prefixed with '#!1 ' or '#!2 ' or '#!3 ', which is 4 characters long
  18. var categories_per_string = 8; // with 20 characters per string, and 2 characters per category, we can fit 8 (plus the 4-char unique id)
  19. var characters_per_category = 2; // with 6 flags, this allows for 11 sub categories and 1 major category
  20. var bit_flags_per_char = 6; // using 01XXXXXX ASCII codes, which allows for 6 flags per character
  21. var category_id = 20; // save the custom fields in the 'uncategorized' major category, which has the id of 20
  22. var class_hidden = 'sgs-hide-from-mint'; // just a unique class; if an element has it, it will be hidden.
  23. var class_edit_mode = 'mint_edit_mode';
  24.  
  25. var hs_action_hide = 'hide';
  26. var hs_action_show = 'show';
  27. var hs_action_edit = 'edit';
  28. var sgs_style_sheet = create_style_sheet();
  29.  
  30. // Need to define all categories and subcategories, along with their ID. Create this list dynamically.
  31. function get_default_category_list() {
  32. var categories = [];
  33.  
  34. // loop through each category and create an array of arrays with their info
  35. jQuery('#popup-cc-L1').find('> li').each(function () {
  36. var category_major = [];
  37. category_major.id = jQuery(this).attr('id').replace(/\D/g, ''); // number-only portion (they all start with 'pop-categories-'{number}
  38. //console.log(category_major.id);
  39. category_major.name = jQuery(this).children('a').text();
  40. category_major.categories_minor = [];
  41. category_major.is_hidden = jQuery(this).hasClass(class_hidden);
  42. /* get the minor/sub categories. only the :first ul, because the second one is
  43. user-defined custom categories, which they can change with native mint.com controls
  44. */
  45. jQuery(this).find('div.popup-cc-L2 ul:first > li').each(function () {
  46. var category_minor = [];
  47. category_minor.id = jQuery(this).attr('id').replace(/\D/g, '');
  48. category_minor.name = jQuery(this).text();
  49. category_minor.is_hidden = jQuery(this).hasClass(class_hidden);
  50. category_major.categories_minor.push(category_minor);
  51. });
  52. categories.push(category_major);
  53. });
  54.  
  55. return categories;
  56. }
  57.  
  58. // Save any categories the user wants hidden.
  59. // This will be done by creating a custom subcategory in the 'uncategorized' category, where the name of this
  60. // subcategory will define which other categories to hide.
  61. // This way, if the UserScript is installed on multiple computers, the user sees their preferences synced.
  62. // Alternatively, a cookie could be used, but preferences would be specific to that device.
  63.  
  64. /**
  65. * @str_bit_array_array Array A printable-ascii encoded bit array (values that can be saved in a custom category)
  66. * @array_of_all_categories Array 8*3 categories with their subcategories and
  67. * ID, name, subcategories and is_hidden members
  68. * for both the category and subcategories
  69. */
  70. function decode_bit_array(str_bit_array_array, array_of_all_categories) {
  71.  
  72. // second, translate any extra characters into their forbidden character
  73. // (double quote is forbidden; use a non-bit-array character to encode)
  74.  
  75. // third, loop through each category and each of its subcategories
  76.  
  77. // use bitwise operators to see if the subcategory minor id (always 1-9) is marked as hidden
  78.  
  79.  
  80. str_bit_array_array.sort();
  81.  
  82. array_of_all_categories.sort(function (a, b) {
  83. return (a.id - b.id); // sort by id, lowest first
  84. });
  85.  
  86.  
  87. var field_count = 0; // 0, 1, or 2; for the 3 unique fields holding the 23 category hidden attributes
  88. var bit_string_category_count = 0; // only 8 categories per string. once this goes past 7, reset and use next string.
  89. var str_bit_array = str_bit_array_array[field_count]; // 3 custom fields with attributes
  90. // remove the first 4 characters (the unique ID plus a space)
  91. str_bit_array = str_bit_array.substring(unique_id_length); // 0-based, meaning start at the fifth character (inclusive)
  92.  
  93. // loop through each major category and its minor categories and mark them as hidden or not
  94. for (var category_major_count = 0, category_major_length = array_of_all_categories.length; category_major_count < category_major_length; category_major_count++) {
  95. var category_major = array_of_all_categories[category_major_count];
  96.  
  97. array_of_all_categories[category_major_count].categories_minor.sort(function (a, b) {
  98. return (a.id - b.id); // sort by id, lowest first
  99. });
  100.  
  101.  
  102. var bit_characters = str_bit_array.substr(bit_string_category_count * characters_per_category, characters_per_category); // grab the 2 characters for this category
  103.  
  104. for (var category_minor_count = 0, category_minor_length = category_major.categories_minor.length; category_minor_count < category_minor_length; category_minor_count++) {
  105.  
  106. var minor_id = category_major.categories_minor[category_minor_count].id.slice(-2); // the last two digits are the minor category; the first two are always the same as the parent major category
  107. // if flag is '1', it is hidden
  108. array_of_all_categories[category_major_count].categories_minor[category_minor_count].is_hidden = is_category_hidden(bit_characters, minor_id);
  109. //console.debug(array_of_all_categories[category_major_count].categories_minor[category_minor_count].name + ' is hidden: '
  110. // + array_of_all_categories[category_major_count].categories_minor[category_minor_count].is_hidden);
  111. }
  112. var last_flag = characters_per_category * bit_flags_per_char; // last flag is used for major category, instead of subcategory #12 (2 * 6);
  113. array_of_all_categories[category_major_count].is_hidden = is_category_hidden(bit_characters, last_flag);
  114. //console.debug(array_of_all_categories[category_major_count].name + ' is hidden: '
  115. // + array_of_all_categories[category_major_count].is_hidden);
  116. bit_string_category_count += 1;
  117. if (bit_string_category_count > (categories_per_string - 1)) {
  118. // each of our custom bit arrays can only hold 8 categories' info. reset the counter and move on to the next bit array
  119. bit_string_category_count = 0;
  120. field_count += 1;
  121.  
  122. str_bit_array = str_bit_array_array[field_count]; // 3 custom fields with attributes
  123. // remove the first 4 characters (the unique ID plus a space)
  124. str_bit_array = str_bit_array.substring(unique_id_length); // 0-based, meaning start at character 5 (inclusive)
  125. }
  126. }
  127. return array_of_all_categories;
  128. }
  129.  
  130. /**
  131. * Returns true if the bit location is set to true.
  132. */
  133. function is_category_hidden(ascii_characters, minor_id) {
  134. var bit_shift_count = ((minor_id - 1) % bit_flags_per_char); // category 1 is stored in last bit flag; cat 2 in the second to last. cat 7 stored in last flag
  135. var bit_to_use = (Math.floor((minor_id - 1) / bit_flags_per_char)); // 1-6 (0-5) in first bit. 7-12 (6-11) stored in second. etc. 7/6 floored is 1.
  136. var bit_character = (ascii_characters.charCodeAt(bit_to_use)); // get binary representation
  137. var is_hidden = ((bit_character >>> bit_shift_count) & 000001); // shift the bits over and mask with '1'. if both are 1, it will return 1 (true) for hidden
  138. //console.log('is_hidden: ' + is_hidden);
  139. return is_hidden;
  140. }
  141.  
  142. function encode_category_hidden(ascii_characters, minor_id, is_hidden) {
  143. var bit_shift_count = ((minor_id - 1) % bit_flags_per_char); // category 1 is stored in last bit flag; cat 2 in the second to last. cat 7 stored in last flag
  144. var bit_to_use = (Math.floor((minor_id - 1) / bit_flags_per_char)); // 1-6 (0-5) in first bit. 7-12 (6-11) stored in second. etc. 7/6 floored is 1.
  145. var mask = ((is_hidden << bit_shift_count)); // move the mask to the proper location
  146.  
  147. var new_char = String.fromCharCode(ascii_characters[bit_to_use].charCodeAt(0) | mask);
  148. ascii_characters = ascii_characters.replaceAt(bit_to_use, new_char); // replace with new character
  149. //console.debug(ascii_characters);
  150. return ascii_characters;
  151. }
  152.  
  153. /**
  154. * Replaces the substituted characters with the 'illegal' characters.
  155. * This way, the script can use bitwise operations in a logical manner.
  156. */
  157. function translate_to_script(string_with_substituted_characters) {
  158. var str_return = string_with_substituted_characters.replace('?', String.fromCharCode(127)); // the delete char (127) is substituted with a question mark when saved at mint
  159. return str_return;
  160. }
  161.  
  162. /**
  163. * Replaces any illegal characters with substituted characters that mint.com allows in text fields.
  164. */
  165. function translate_to_mint(string_with_illegal_characters) {
  166. var str_return = string_with_illegal_characters.replace(String.fromCharCode(127), '?');
  167. return str_return;
  168. }
  169.  
  170. /**
  171. * Extracts the bit arrays from the saved custom category input box and puts all 3 into a string array.
  172. * @returns {Array}
  173. */
  174. function extract_mint_array() {
  175. var str_bit_array_array = [];
  176. for (var array_number = 1; array_number <= number_of_bit_arrays; array_number++){
  177. str_bit_array_array.push(jQuery('#menu-category-' + category_id + ' ul li:contains("#!' + array_number + '"):first').text());
  178. //console.log('processing val is ' + jQuery(this).text());
  179. }
  180. return str_bit_array_array;
  181. }
  182.  
  183. function encode_bit_array(array_of_all_categories) {
  184. // loop through each category, create an ASCII character, and replace illegal characters
  185.  
  186. array_of_all_categories.sort(function (a, b) {
  187. return (a.id - b.id); // sort by id, lowest first
  188. });
  189.  
  190. var field_count = 0; // 0, 1, or 2; for the 3 unique fields holding the 23 category hidden attributes
  191. var bit_string_category_count = 0; // only 8 categories per string. once this goes past 7, reset and use next string.
  192. var str_bit_array_array = [];
  193.  
  194. // loop through each major category and its minor categories and mark them as hidden or not
  195. str_bit_array_array[field_count] = new Array(characters_per_category * categories_per_string + 1).join('@');
  196. for (var category_major_count = 0, category_major_length = array_of_all_categories.length; category_major_count < category_major_length; category_major_count++) {
  197.  
  198. var category_major = array_of_all_categories[category_major_count];
  199.  
  200. array_of_all_categories[category_major_count].categories_minor.sort(function (a, b) {
  201. return (a.id - b.id); // sort by id, lowest first
  202. });
  203.  
  204.  
  205. var bit_characters = new Array(characters_per_category + 1).join('@'); // the @ character is 01000000, so all flags (last 6) start 'off'.
  206. for (var category_minor_count = 0, category_minor_length = category_major.categories_minor.length; category_minor_count < category_minor_length; category_minor_count++) {
  207.  
  208. var minor_id = category_major.categories_minor[category_minor_count].id.slice(-2); // the last two digits are the minor category; the first two are always the same as the parent major category
  209. // if flag is '1', it is hidden
  210.  
  211. bit_characters = encode_category_hidden(bit_characters, minor_id, array_of_all_categories[category_major_count].categories_minor[category_minor_count].is_hidden);
  212. str_bit_array_array[field_count] = str_bit_array_array[field_count].replaceAt(bit_string_category_count * characters_per_category, bit_characters);
  213. }
  214. var last_flag = characters_per_category * bit_flags_per_char; // last flag is used for major category, instead of subcategory #12 (2 * 6);
  215. bit_characters = encode_category_hidden(bit_characters, last_flag, array_of_all_categories[category_major_count].is_hidden);
  216.  
  217. str_bit_array_array[field_count] = str_bit_array_array[field_count].replaceAt(bit_string_category_count * characters_per_category, bit_characters);
  218.  
  219. bit_string_category_count += 1;
  220. if (bit_string_category_count > (categories_per_string - 1)) {
  221. // each of our custom bit arrays can only hold 8 categories' info. reset the counter and move on to the next bit array
  222. bit_string_category_count = 0;
  223. field_count += 1;
  224. str_bit_array_array[field_count] = new Array(characters_per_category * categories_per_string + 1).join('@');
  225. }
  226. }
  227. // now that all 3 bit arrays are creates, tack on the unique id to the string
  228. for (var i = 0; i < number_of_bit_arrays; i++) {
  229. str_bit_array_array[i] = '#!' + (i + 1) + ' ' + str_bit_array_array[i];
  230. str_bit_array_array[i] = translate_to_mint(str_bit_array_array[i]);
  231. //console.log(str_bit_array_array[i]);
  232. }
  233. return str_bit_array_array;
  234. }
  235.  
  236. function clean_up_extra_fields(bit_string) {
  237. var unique_id = bit_string.substr(0, unique_id_length);
  238. jQuery('ul.popup-cc-L2-custom > li > input[value^="' + unique_id + '"]:not(:first)').each(function () {
  239. var input_id = jQuery(this).prev().val();
  240. delete_field(input_id);
  241. });
  242. }
  243.  
  244. /**
  245. * Will update or insert as needed.
  246. * @param bit_string
  247. */
  248. function upsert_field(bit_string) {
  249. var unique_id = bit_string.substr(0, unique_id_length);
  250. var input_id = jQuery('ul.popup-cc-L2-custom > li > input[value^="' + unique_id + '"]:first').prev().val();
  251. if (input_id) {
  252. update_field(bit_string);
  253. } else {
  254. insert_field(bit_string);
  255. }
  256. }
  257.  
  258. function insert_field(bit_string) {
  259. var hidden_token = JSON.parse(jQuery('#javascript-user').val()).token;
  260. var data = {
  261. pcatId: category_id,
  262. catId: 0,
  263. category: bit_string,
  264. task: 'C',
  265. token: hidden_token
  266. };
  267. jQuery.ajax(
  268. {
  269. type: "POST",
  270. url: '/updateCategory.xevent',
  271. data: data
  272. }
  273. );
  274. }
  275.  
  276. function update_field(bit_string) {
  277.  
  278. var hidden_token = JSON.parse(jQuery('#javascript-user').val()).token;
  279. var unique_id = bit_string.substr(0, unique_id_length);
  280.  
  281. var input = jQuery('ul.popup-cc-L2-custom > li > input[value^="' + unique_id + '"]:first');
  282. //console.log('setting input string from ' + input.val() + ' to ' + bit_string);
  283. input.val(bit_string); // set the value on the user's page manually (not needed for ajax, but needed for later processing)
  284. //jQuery('#menu-category-' + category_id + ' ul li:contains("' + unique_id + '")').text(bit_string); //@TODO what is this line doing?
  285. /* input.prop('value',bit_string);
  286. input.attr('value',bit_string);*/
  287. var input_id = input.prev().val();
  288. var data = {
  289. pcatId: category_id,
  290. catId: input_id,
  291. category: bit_string,
  292. task: 'U',
  293. token: hidden_token
  294. };
  295. jQuery.ajax(
  296. {
  297. type: "POST",
  298. url: '/updateCategory.xevent',
  299. data: data
  300. }
  301. );
  302. }
  303.  
  304. function delete_field(input_id) {
  305. var hidden_token = JSON.parse(jQuery('#javascript-user').val()).token;
  306. var data = {
  307. catId: input_id,
  308. task: 'D',
  309. token: hidden_token
  310. };
  311. jQuery.ajax(
  312. {
  313. type: "POST",
  314. url: '/updateCategory.xevent',
  315. data: data
  316. }
  317. );
  318. }
  319.  
  320. /**
  321. * Loop through all the category objects. If any are hidden,
  322. * add the proper CSS to hide the field.
  323. * Also, remove any line-throughs, in case a hidden category has been restored.
  324. * @param default_categories
  325. */
  326. function process_hidden_categories(default_categories) {
  327. clearSGSStyle(); // clear css rules first. these rules define elements that are hidden, based on their id.
  328. for (var major_count = 0; major_count < default_categories.length; major_count++) {
  329. for (var minor_count = 0; minor_count < default_categories[major_count].categories_minor.length; minor_count++) {
  330. if (default_categories[major_count].categories_minor[minor_count].is_hidden) {
  331. jQuery('#menu-category-' + default_categories[major_count].categories_minor[minor_count].id).addClass(class_hidden);
  332. css_hide_element('#menu-category-' + default_categories[major_count].categories_minor[minor_count].id);
  333. //console.log('hide minor ' + default_categories[major_count].categories_minor[minor_count].id);
  334. jQuery('#pop-categories-' + default_categories[major_count].categories_minor[minor_count].id).addClass(class_hidden);
  335. }
  336. jQuery('#menu-category-' + default_categories[major_count].categories_minor[minor_count].id).css('text-decoration', '');
  337. jQuery('#pop-categories-' + default_categories[major_count].categories_minor[minor_count].id).css('text-decoration', '');
  338. }
  339. if (default_categories[major_count].is_hidden) {
  340. jQuery('#menu-category-' + default_categories[major_count].id).addClass(class_hidden);
  341. jQuery('#pop-categories-' + default_categories[major_count].id).addClass(class_hidden);
  342.  
  343. }
  344. // insert rename here
  345. jQuery('#menu-category-' + default_categories[major_count].id).css('text-decoration', '');
  346. jQuery('#pop-categories-' + default_categories[major_count].id).css('text-decoration', '');
  347.  
  348. }
  349. hide_show_category(hs_action_hide);
  350. }
  351. /**
  352. * Recently, mint has been removing all classes of elements after the dropdown
  353. * is shown. This unfortunately removes my hiding class, causing subcategories
  354. * to be shown after briefly being hidden.
  355. * Therefore, this function was created to make a global css rule to hide
  356. * and show elements based on their id, rather than by adding classes.
  357. */
  358. function css_hide_element(element_id){
  359. addSGSStyle(element_id, 'display: none');
  360. }
  361.  
  362. /**
  363. *
  364. * @param action
  365. */
  366. function hide_show_category(action) {
  367. var category = jQuery('.' + class_hidden);
  368. if (action == hs_action_hide) {
  369. // hide the categories completely
  370. category.hide();
  371. category.css('text-decoration', 'line-through');
  372. }
  373. if (action == hs_action_show) {
  374. // remove any visible attributes
  375. category.show();
  376. category.css('text-decoration', '');
  377. }
  378. if (action == hs_action_edit) {
  379. category.show();
  380. category.css('text-decoration', 'line-through');
  381. }
  382. }
  383.  
  384. function add_toggle() {
  385. if (!(jQuery('#sgs-toggle').length)) {
  386. var toggle_style = "position: absolute; right: 30px; top: 35px; cursor: pointer";
  387. var toggle_text = "Edit Hidden Categories";
  388. jQuery('#pop-categories-main').prepend('<div id="sgs-toggle" class="" style="' + toggle_style + '">' + toggle_text + '</div>');
  389. jQuery('#sgs-toggle').click(function () {
  390. edit_categories();
  391. });
  392. jQuery('#pop-categories-submit').click(function(){
  393. jQuery('#sgs-toggle').addClass('editing'); // force the current mode to be editing so the edit_categories call saves
  394. edit_categories(); // if user clicks the "I'm done" button, we should also save the categories.
  395. })
  396. }
  397. }
  398.  
  399. function edit_categories() {
  400. var toggle = jQuery('#sgs-toggle');
  401. toggle.toggleClass('editing');
  402. if (toggle.hasClass('editing')) {
  403. toggle.text('Save Hidden Categories');
  404. mint_edit(true); // make all categories clickable; when clicked, add class and strike out
  405. } else {
  406. mint_edit(false); // remove clickable event and strike css; go back to hiding
  407. mint_save();
  408. toggle.text('Edit Hidden Categories');
  409.  
  410. }
  411.  
  412. }
  413.  
  414. function mint_edit(edit_mode) {
  415. if (edit_mode) {
  416. // get the major and minor categories in the popup editor
  417. var minor_categories = jQuery('div.popup-cc-L2 > ul:first-of-type > li'); // second ul is custom categories, so just get first
  418. var major_categories = jQuery('#popup-cc-L1').find('.isL1');
  419.  
  420.  
  421. // display all previously hidden fields (except our three custom fields holding bit arrays)
  422.  
  423.  
  424. // add checkboxes to the categories
  425. minor_categories.each(function () {
  426. add_checkbox(this);
  427. });
  428. major_categories.each(function () {
  429. add_checkbox(this);
  430. })
  431. jQuery('input.hide_show_checkbox').css({
  432. 'position': 'absolute',
  433. 'right': '-18px'
  434. });
  435.  
  436. // add label for minor checkboxes
  437. jQuery('div.popup-cc-L2 > h3:first-of-type').append('<span class="' + class_edit_mode + ' minor_hide_show_label">Hide</span>');
  438. jQuery('span.minor_hide_show_label').css({
  439. 'position': 'absolute',
  440. 'right': '64px',
  441. 'top': '3px',
  442. 'font-size': '13px',
  443. 'font-weight': 'bold'
  444. });
  445.  
  446.  
  447. // add label for major categories
  448. jQuery('#pop-categories-form fieldset').prepend('<span class="' + class_edit_mode + ' major_hide_show_label">Hide</span>');
  449. jQuery('span.major_hide_show_label').css({
  450. 'position': 'absolute',
  451. 'left': '267px',
  452. 'font-size': '13px',
  453. 'font-weight': 'bold'
  454. });
  455. // add checkbox event. when checked add the 'hidden' class (which is scanned on save)
  456. jQuery('input.hide_show_checkbox').click(function () {
  457. var parent_id = jQuery(this).parent().attr('id').replace(/\D/g, '');
  458. if (jQuery(this).is(':checked')) {
  459. jQuery('#menu-category-' + parent_id).addClass(class_hidden);
  460. jQuery('#pop-categories-' + parent_id).addClass(class_hidden);
  461. jQuery(this).parent().css('text-decoration', 'line-through');
  462. } else {
  463. jQuery('#menu-category-' + parent_id).removeClass(class_hidden);
  464. jQuery('#pop-categories-' + parent_id).removeClass(class_hidden);
  465. ;
  466. jQuery(this).parent().css('text-decoration', '');
  467.  
  468. }
  469. });
  470. hide_show_category(hs_action_edit);
  471. } else {
  472. // no longer editing (saving), so remove our labels and checkboxes, and re-hide the desired categories
  473. jQuery('.' + class_edit_mode).remove();
  474. hide_show_category(hs_action_hide);
  475. }
  476. }
  477.  
  478. function add_checkbox(element) {
  479. var checked = '';
  480. if (jQuery(element).hasClass(class_hidden)) {
  481. // hidden categories are checked
  482. checked = 'checked="checked"';
  483. }
  484. jQuery(element).append('<input type="checkbox" class="' + class_edit_mode + ' hide_show_checkbox"' + checked + ' />');
  485. }
  486.  
  487. /**
  488. * hooks to the save or cancel button so that hidden categories will be re-hidden after ajax refresh
  489. */
  490. function add_save_hook() {
  491. if ((jQuery('#pop-categories-submit').length && (!(jQuery('#pop-categories-submit').hasClass('sgs-hook-added'))))) {
  492.  
  493. jQuery('#pop-categories-submit').addClass('sgs-hook-added');
  494. jQuery('#pop-categories-submit, #pop-categories-close').click(function () {
  495. mint_refresh();
  496. });
  497. }
  498. }
  499.  
  500. /**
  501. * Hides our bit array custom categories permanently so the user won't accidentally mess with them.
  502. */
  503. function hide_bit_array() {
  504. jQuery('input[value^="#!"]').parent().hide();
  505. jQuery('li[id^="menu-category-"] a:contains("#!")').parent().hide();
  506. jQuery('li[id^="menu-category-"] a:contains("#!")').each(function( index ) {
  507. // use the new css global stylesheet function to hide the element as well.
  508. css_hide_element("#" + jQuery(this).parent().attr('id'));
  509. });
  510. }
  511.  
  512. /**
  513. * Allows immutable strings to have character(s) replaced
  514. * @param index
  515. * @param character
  516. * @returns {string}
  517. */
  518. String.prototype.replaceAt = function (index, character) {
  519. return this.substr(0, index) + character + this.substr(index + character.length);
  520. };
  521.  
  522. /**
  523. * Lets you bind an event and have it run first.
  524. * @param name
  525. * @param fn
  526. */
  527. jQuery.fn.bindFirst = function (name, fn) {
  528. // bind as you normally would
  529. // don't want to miss out on any jQuery magic
  530. this.on(name, fn);
  531.  
  532. // Thanks to a comment by @Martin, adding support for
  533. // namespaced events too.
  534. this.each(function () {
  535. var handlers = jQuery._data(this, 'events')[name.split('.')[0]];
  536. // take out the handler we just inserted from the end
  537. var handler = handlers.pop();
  538. // move it at the beginning
  539. handlers.splice(0, 0, handler);
  540. });
  541. };
  542.  
  543. function mint_refresh() {
  544. add_toggle();
  545. add_save_hook();
  546. // when the popup is opened or closed, re-hide the categories
  547. var str_bit_array_array = extract_mint_array();
  548. var default_categories = get_default_category_list();
  549. default_categories = decode_bit_array(str_bit_array_array, default_categories);
  550. process_hidden_categories(default_categories); // hides the appropriate fields
  551.  
  552. //Hides our three fields, since the user probably shouldn't mess with them directly (and they look weird).
  553. hide_bit_array(); // comment this out in order to see the bit string data
  554. }
  555.  
  556. /**
  557. * Saves the preferences
  558. */
  559. function mint_save() {
  560. //console.debug('saving');
  561. var default_categories = get_default_category_list();
  562. var bit_array_array = encode_bit_array(default_categories);
  563. for (var i = 0; i < bit_array_array.length; i++) {
  564. clean_up_extra_fields(bit_array_array[i]); // delete any extra preference fields my older script created
  565. upsert_field(bit_array_array[i]);
  566. }
  567. }
  568. function add_dropdown_hook() {
  569. //console.log('start dropdown');
  570. //if ((jQuery('#txnEdit-category_input').length) && (!(jQuery('#txtEdit-category_input').hasClass('sgs-hook-added')))) {
  571. jQuery('#txnEdit-category_input').addClass('sgs-hook-added');
  572. jQuery('#txnEdit-category_input, #txnEdit-category_picker').off('click', mint_refresh);
  573. jQuery('#txnEdit-category_input, #txnEdit-category_picker').on('click', mint_refresh);
  574. //}
  575. }
  576. /**
  577. * A bonus feature of this script: Fixes Mint's google search query.
  578. * (If a transaction description contains a space, quote or other URI character,
  579. * mint.com doesn't encode it, which causes the google search link to be invalid.)
  580. */
  581. function google_search_fix(){
  582. jQuery('#txnEdit-toggle').on('click', function(){
  583. // get the text; don't try decoding the partial original search link
  584. plain_search = jQuery('a.desc_link strong var').text();
  585.  
  586. // proper encoding
  587. new_search = encodeURIComponent(plain_search);
  588.  
  589. // encoding changes spaces to %20, but that is deprecated now. urls take '+' instead.
  590. new_search = new_search.replace(/%20/gi, '+');
  591.  
  592. // replace old url with new
  593. jQuery('a.desc_link').attr('href', 'https://www.google.com/#q=' + new_search);
  594. });
  595. }
  596. /**
  597. * Function to add a global css style to a page.
  598. * https://davidwalsh.name/add-rules-stylesheets
  599. * This function is called once, at the top of the code, storing
  600. * the new stylesheet reference in a script-global variable.
  601. */
  602. function create_style_sheet(){
  603. // Create the <style> tag
  604. var style = document.createElement("style");
  605.  
  606. // Add a media (and/or media query) here if you'd like!
  607. // style.setAttribute("media", "screen")
  608. // style.setAttribute("media", "only screen and (max-width : 1024px)")
  609.  
  610. // WebKit hack :(
  611. style.appendChild(document.createTextNode(""));
  612.  
  613. // Add the <style> element to the page
  614. document.head.appendChild(style);
  615.  
  616. return style.sheet;
  617. }
  618. // Cross compatible function to insert a rule. Index is optional.
  619. function _addCSSRule(sheet, selector, rules, index) {
  620.  
  621. if("insertRule" in sheet) {
  622. sheet.insertRule(selector + "{" + rules + "}", index);
  623. }
  624. else if("addRule" in sheet) {
  625. sheet.addRule(selector, rules, index);
  626. }
  627.  
  628. }
  629. /*
  630. * This is the actual function called to hide an element via its id.
  631. */
  632. function addSGSStyle(css_selector, css_rule) {
  633. _addCSSRule(sgs_style_sheet, css_selector, css_rule);
  634. }
  635. /*
  636. * Rather than removing specific rules, we just wipe the entire sheet out.
  637. * Afterwards, the rules are recreated to fit the new preferences.
  638. */
  639. function clearSGSStyle() {
  640.  
  641. while (sgs_style_sheet.cssRules.length > 0){
  642.  
  643. sgs_style_sheet.deleteRule(0);
  644. }
  645. }
  646. ////// Start jQuery Addon - 660-1123
  647. if (!(jQuery.fn.arrive)){
  648. /*globals jQuery,Window,HTMLElement,HTMLDocument,HTMLCollection,NodeList,MutationObserver */
  649. /*exported Arrive*/
  650. /*jshint latedef:false */
  651.  
  652. /*
  653. * arrive.js
  654. * v2.4.1
  655. * https://github.com/uzairfarooq/arrive
  656. * MIT licensed
  657. *
  658. * Copyright (c) 2014-2017 Uzair Farooq
  659. */
  660. var Arrive = (function(window, $, undefined) {
  661.  
  662. "use strict";
  663.  
  664. if(!window.MutationObserver || typeof HTMLElement === 'undefined'){
  665. return; //for unsupported browsers
  666. }
  667.  
  668. var arriveUniqueId = 0;
  669.  
  670. var utils = (function() {
  671. var matches = HTMLElement.prototype.matches || HTMLElement.prototype.webkitMatchesSelector || HTMLElement.prototype.mozMatchesSelector
  672. || HTMLElement.prototype.msMatchesSelector;
  673.  
  674. return {
  675. matchesSelector: function(elem, selector) {
  676. return elem instanceof HTMLElement && matches.call(elem, selector);
  677. },
  678. // to enable function overloading - By John Resig (MIT Licensed)
  679. addMethod: function (object, name, fn) {
  680. var old = object[ name ];
  681. object[ name ] = function(){
  682. if ( fn.length == arguments.length ) {
  683. return fn.apply( this, arguments );
  684. }
  685. else if ( typeof old == 'function' ) {
  686. return old.apply( this, arguments );
  687. }
  688. };
  689. },
  690. callCallbacks: function(callbacksToBeCalled, registrationData) {
  691. if (registrationData && registrationData.options.onceOnly && registrationData.firedElems.length == 1) {
  692. // as onlyOnce param is true, make sure we fire the event for only one item
  693. callbacksToBeCalled = [callbacksToBeCalled[0]];
  694. }
  695.  
  696. for (var i = 0, cb; (cb = callbacksToBeCalled[i]); i++) {
  697. if (cb && cb.callback) {
  698. cb.callback.call(cb.elem, cb.elem);
  699. }
  700. }
  701.  
  702. if (registrationData && registrationData.options.onceOnly && registrationData.firedElems.length == 1) {
  703. // unbind event after first callback as onceOnly is true.
  704. registrationData.me.unbindEventWithSelectorAndCallback.call(
  705. registrationData.target, registrationData.selector, registrationData.callback);
  706. }
  707. },
  708. // traverse through all descendants of a node to check if event should be fired for any descendant
  709. checkChildNodesRecursively: function(nodes, registrationData, matchFunc, callbacksToBeCalled) {
  710. // check each new node if it matches the selector
  711. for (var i=0, node; (node = nodes[i]); i++) {
  712. if (matchFunc(node, registrationData, callbacksToBeCalled)) {
  713. callbacksToBeCalled.push({ callback: registrationData.callback, elem: node });
  714. }
  715.  
  716. if (node.childNodes.length > 0) {
  717. utils.checkChildNodesRecursively(node.childNodes, registrationData, matchFunc, callbacksToBeCalled);
  718. }
  719. }
  720. },
  721. mergeArrays: function(firstArr, secondArr){
  722. // Overwrites default options with user-defined options.
  723. var options = {},
  724. attrName;
  725. for (attrName in firstArr) {
  726. if (firstArr.hasOwnProperty(attrName)) {
  727. options[attrName] = firstArr[attrName];
  728. }
  729. }
  730. for (attrName in secondArr) {
  731. if (secondArr.hasOwnProperty(attrName)) {
  732. options[attrName] = secondArr[attrName];
  733. }
  734. }
  735. return options;
  736. },
  737. toElementsArray: function (elements) {
  738. // check if object is an array (or array like object)
  739. // Note: window object has .length property but it's not array of elements so don't consider it an array
  740. if (typeof elements !== "undefined" && (typeof elements.length !== "number" || elements === window)) {
  741. elements = [elements];
  742. }
  743. return elements;
  744. }
  745. };
  746. })();
  747.  
  748.  
  749. // Class to maintain state of all registered events of a single type
  750. var EventsBucket = (function() {
  751. var EventsBucket = function() {
  752. // holds all the events
  753.  
  754. this._eventsBucket = [];
  755. // function to be called while adding an event, the function should do the event initialization/registration
  756. this._beforeAdding = null;
  757. // function to be called while removing an event, the function should do the event destruction
  758. this._beforeRemoving = null;
  759. };
  760.  
  761. EventsBucket.prototype.addEvent = function(target, selector, options, callback) {
  762. var newEvent = {
  763. target: target,
  764. selector: selector,
  765. options: options,
  766. callback: callback,
  767. firedElems: []
  768. };
  769.  
  770. if (this._beforeAdding) {
  771. this._beforeAdding(newEvent);
  772. }
  773.  
  774. this._eventsBucket.push(newEvent);
  775. return newEvent;
  776. };
  777.  
  778. EventsBucket.prototype.removeEvent = function(compareFunction) {
  779. for (var i=this._eventsBucket.length - 1, registeredEvent; (registeredEvent = this._eventsBucket[i]); i--) {
  780. if (compareFunction(registeredEvent)) {
  781. if (this._beforeRemoving) {
  782. this._beforeRemoving(registeredEvent);
  783. }
  784.  
  785. // mark callback as null so that even if an event mutation was already triggered it does not call callback
  786. var removedEvents = this._eventsBucket.splice(i, 1);
  787. if (removedEvents && removedEvents.length) {
  788. removedEvents[0].callback = null;
  789. }
  790. }
  791. }
  792. };
  793.  
  794. EventsBucket.prototype.beforeAdding = function(beforeAdding) {
  795. this._beforeAdding = beforeAdding;
  796. };
  797.  
  798. EventsBucket.prototype.beforeRemoving = function(beforeRemoving) {
  799. this._beforeRemoving = beforeRemoving;
  800. };
  801.  
  802. return EventsBucket;
  803. })();
  804.  
  805.  
  806. /**
  807. * @constructor
  808. * General class for binding/unbinding arrive and leave events
  809. */
  810. var MutationEvents = function(getObserverConfig, onMutation) {
  811. var eventsBucket = new EventsBucket(),
  812. me = this;
  813.  
  814. var defaultOptions = {
  815. fireOnAttributesModification: false
  816. };
  817.  
  818. // actual event registration before adding it to bucket
  819. eventsBucket.beforeAdding(function(registrationData) {
  820. var
  821. target = registrationData.target,
  822. observer;
  823.  
  824. // mutation observer does not work on window or document
  825. if (target === window.document || target === window) {
  826. target = document.getElementsByTagName("html")[0];
  827. }
  828.  
  829. // Create an observer instance
  830. observer = new MutationObserver(function(e) {
  831. onMutation.call(this, e, registrationData);
  832. });
  833.  
  834. var config = getObserverConfig(registrationData.options);
  835.  
  836. observer.observe(target, config);
  837.  
  838. registrationData.observer = observer;
  839. registrationData.me = me;
  840. });
  841.  
  842. // cleanup/unregister before removing an event
  843. eventsBucket.beforeRemoving(function (eventData) {
  844. eventData.observer.disconnect();
  845. });
  846.  
  847. this.bindEvent = function(selector, options, callback) {
  848. options = utils.mergeArrays(defaultOptions, options);
  849.  
  850. var elements = utils.toElementsArray(this);
  851.  
  852. for (var i = 0; i < elements.length; i++) {
  853. eventsBucket.addEvent(elements[i], selector, options, callback);
  854. }
  855. };
  856.  
  857. this.unbindEvent = function() {
  858. var elements = utils.toElementsArray(this);
  859. eventsBucket.removeEvent(function(eventObj) {
  860. for (var i = 0; i < elements.length; i++) {
  861. if (this === undefined || eventObj.target === elements[i]) {
  862. return true;
  863. }
  864. }
  865. return false;
  866. });
  867. };
  868.  
  869. this.unbindEventWithSelectorOrCallback = function(selector) {
  870. var elements = utils.toElementsArray(this),
  871. callback = selector,
  872. compareFunction;
  873.  
  874. if (typeof selector === "function") {
  875. compareFunction = function(eventObj) {
  876. for (var i = 0; i < elements.length; i++) {
  877. if ((this === undefined || eventObj.target === elements[i]) && eventObj.callback === callback) {
  878. return true;
  879. }
  880. }
  881. return false;
  882. };
  883. }
  884. else {
  885. compareFunction = function(eventObj) {
  886. for (var i = 0; i < elements.length; i++) {
  887. if ((this === undefined || eventObj.target === elements[i]) && eventObj.selector === selector) {
  888. return true;
  889. }
  890. }
  891. return false;
  892. };
  893. }
  894. eventsBucket.removeEvent(compareFunction);
  895. };
  896.  
  897. this.unbindEventWithSelectorAndCallback = function(selector, callback) {
  898. var elements = utils.toElementsArray(this);
  899. eventsBucket.removeEvent(function(eventObj) {
  900. for (var i = 0; i < elements.length; i++) {
  901. if ((this === undefined || eventObj.target === elements[i]) && eventObj.selector === selector && eventObj.callback === callback) {
  902. return true;
  903. }
  904. }
  905. return false;
  906. });
  907. };
  908.  
  909. return this;
  910. };
  911.  
  912.  
  913. /**
  914. * @constructor
  915. * Processes 'arrive' events
  916. */
  917. var ArriveEvents = function() {
  918. // Default options for 'arrive' event
  919. var arriveDefaultOptions = {
  920. fireOnAttributesModification: false,
  921. onceOnly: false,
  922. existing: false
  923. };
  924.  
  925. function getArriveObserverConfig(options) {
  926. var config = {
  927. attributes: false,
  928. childList: true,
  929. subtree: true
  930. };
  931.  
  932. if (options.fireOnAttributesModification) {
  933. config.attributes = true;
  934. }
  935.  
  936. return config;
  937. }
  938.  
  939. function onArriveMutation(mutations, registrationData) {
  940. mutations.forEach(function( mutation ) {
  941. var newNodes = mutation.addedNodes,
  942. targetNode = mutation.target,
  943. callbacksToBeCalled = [],
  944. node;
  945.  
  946. // If new nodes are added
  947. if( newNodes !== null && newNodes.length > 0 ) {
  948. utils.checkChildNodesRecursively(newNodes, registrationData, nodeMatchFunc, callbacksToBeCalled);
  949. }
  950. else if (mutation.type === "attributes") {
  951. if (nodeMatchFunc(targetNode, registrationData, callbacksToBeCalled)) {
  952. callbacksToBeCalled.push({ callback: registrationData.callback, elem: targetNode });
  953. }
  954. }
  955.  
  956. utils.callCallbacks(callbacksToBeCalled, registrationData);
  957. });
  958. }
  959.  
  960. function nodeMatchFunc(node, registrationData, callbacksToBeCalled) {
  961. // check a single node to see if it matches the selector
  962. if (utils.matchesSelector(node, registrationData.selector)) {
  963. if(node._id === undefined) {
  964. node._id = arriveUniqueId++;
  965. }
  966. // make sure the arrive event is not already fired for the element
  967. if (registrationData.firedElems.indexOf(node._id) == -1) {
  968. registrationData.firedElems.push(node._id);
  969.  
  970. return true;
  971. }
  972. }
  973.  
  974. return false;
  975. }
  976.  
  977. arriveEvents = new MutationEvents(getArriveObserverConfig, onArriveMutation);
  978.  
  979. var mutationBindEvent = arriveEvents.bindEvent;
  980.  
  981. // override bindEvent function
  982. arriveEvents.bindEvent = function(selector, options, callback) {
  983.  
  984. if (typeof callback === "undefined") {
  985. callback = options;
  986. options = arriveDefaultOptions;
  987. } else {
  988. options = utils.mergeArrays(arriveDefaultOptions, options);
  989. }
  990.  
  991. var elements = utils.toElementsArray(this);
  992.  
  993. if (options.existing) {
  994. var existing = [];
  995.  
  996. for (var i = 0; i < elements.length; i++) {
  997. var nodes = elements[i].querySelectorAll(selector);
  998. for (var j = 0; j < nodes.length; j++) {
  999. existing.push({ callback: callback, elem: nodes[j] });
  1000. }
  1001. }
  1002.  
  1003. // no need to bind event if the callback has to be fired only once and we have already found the element
  1004. if (options.onceOnly && existing.length) {
  1005. return callback.call(existing[0].elem, existing[0].elem);
  1006. }
  1007.  
  1008. setTimeout(utils.callCallbacks, 1, existing);
  1009. }
  1010.  
  1011. mutationBindEvent.call(this, selector, options, callback);
  1012. };
  1013.  
  1014. return arriveEvents;
  1015. };
  1016.  
  1017.  
  1018. /**
  1019. * @constructor
  1020. * Processes 'leave' events
  1021. */
  1022. var LeaveEvents = function() {
  1023. // Default options for 'leave' event
  1024. var leaveDefaultOptions = {};
  1025.  
  1026. function getLeaveObserverConfig() {
  1027. var config = {
  1028. childList: true,
  1029. subtree: true
  1030. };
  1031.  
  1032. return config;
  1033. }
  1034.  
  1035. function onLeaveMutation(mutations, registrationData) {
  1036. mutations.forEach(function( mutation ) {
  1037. var removedNodes = mutation.removedNodes,
  1038. callbacksToBeCalled = [];
  1039.  
  1040. if( removedNodes !== null && removedNodes.length > 0 ) {
  1041. utils.checkChildNodesRecursively(removedNodes, registrationData, nodeMatchFunc, callbacksToBeCalled);
  1042. }
  1043.  
  1044. utils.callCallbacks(callbacksToBeCalled, registrationData);
  1045. });
  1046. }
  1047.  
  1048. function nodeMatchFunc(node, registrationData) {
  1049. return utils.matchesSelector(node, registrationData.selector);
  1050. }
  1051.  
  1052. leaveEvents = new MutationEvents(getLeaveObserverConfig, onLeaveMutation);
  1053.  
  1054. var mutationBindEvent = leaveEvents.bindEvent;
  1055.  
  1056. // override bindEvent function
  1057. leaveEvents.bindEvent = function(selector, options, callback) {
  1058.  
  1059. if (typeof callback === "undefined") {
  1060. callback = options;
  1061. options = leaveDefaultOptions;
  1062. } else {
  1063. options = utils.mergeArrays(leaveDefaultOptions, options);
  1064. }
  1065.  
  1066. mutationBindEvent.call(this, selector, options, callback);
  1067. };
  1068.  
  1069. return leaveEvents;
  1070. };
  1071.  
  1072.  
  1073. var arriveEvents = new ArriveEvents(),
  1074. leaveEvents = new LeaveEvents();
  1075.  
  1076. function exposeUnbindApi(eventObj, exposeTo, funcName) {
  1077. // expose unbind function with function overriding
  1078. utils.addMethod(exposeTo, funcName, eventObj.unbindEvent);
  1079. utils.addMethod(exposeTo, funcName, eventObj.unbindEventWithSelectorOrCallback);
  1080. utils.addMethod(exposeTo, funcName, eventObj.unbindEventWithSelectorAndCallback);
  1081. }
  1082.  
  1083. /*** expose APIs ***/
  1084. function exposeApi(exposeTo) {
  1085. exposeTo.arrive = arriveEvents.bindEvent;
  1086. exposeUnbindApi(arriveEvents, exposeTo, "unbindArrive");
  1087.  
  1088. exposeTo.leave = leaveEvents.bindEvent;
  1089. exposeUnbindApi(leaveEvents, exposeTo, "unbindLeave");
  1090. }
  1091.  
  1092. if ($) {
  1093. exposeApi($.fn);
  1094. }
  1095. exposeApi(HTMLElement.prototype);
  1096. exposeApi(NodeList.prototype);
  1097. exposeApi(HTMLCollection.prototype);
  1098. exposeApi(HTMLDocument.prototype);
  1099. exposeApi(Window.prototype);
  1100.  
  1101. var Arrive = {};
  1102. // expose functions to unbind all arrive/leave events
  1103. exposeUnbindApi(arriveEvents, Arrive, "unbindAllArrive");
  1104. exposeUnbindApi(leaveEvents, Arrive, "unbindAllLeave");
  1105.  
  1106. return Arrive;
  1107.  
  1108. })(window, typeof jQuery === 'undefined' ? null : jQuery, undefined);
  1109. }
  1110. ////// End jQuery Addon
  1111. jQuery(document).arrive("#txnEdit-category_input", add_dropdown_hook);
  1112. jQuery(document).arrive("#txnEdit-toggle", google_search_fix);
  1113.  
  1114. }
  1115.  
  1116.  
  1117. /**
  1118. * Mint.com loads jquery after page is loaded, and it conflicts with other verions. We can't
  1119. * use a sandbox for our script, either, since we must use their version of jquery in order
  1120. * to hook into their ajax completion events.
  1121. * Therefore, we must manually check for jquery every so often (50 ms) until it finally exists.
  1122. * Then, we can call our jquery-requiring function and modify the page.
  1123. * @param method
  1124. */
  1125. function defer(method) {
  1126. if (window.jQuery) {
  1127. method();
  1128. } else {
  1129. setTimeout(function () {
  1130. defer(method);
  1131. }, 50);
  1132. }
  1133. }
  1134. window.addEventListener('load', function () {
  1135. defer(after_jquery);
  1136. });
  1137. }());
  1138.