Mint.com tags display

Show tags in the transactions listing on Mint.com.

目前為 2016-09-15 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Mint.com tags display
  3. // @include https://*.mint.com/*
  4. // @include https://mint.intuit.com/*
  5. // @description Show tags in the transactions listing on Mint.com.
  6. // @namespace com.warkmilson.mint.js
  7. // @author Mark Wilson
  8. // @version 1.2.0
  9. // @homepage https://github.com/mddub/mint-tags-display
  10. // @grant none
  11. // @noframes
  12. // ==/UserScript==
  13. //
  14.  
  15. (function() {
  16. var TAG_STYLE = 'font-size: 10px; display: inline-block;';
  17. var SINGLE_TAG_STYLE = 'margin-left: 4px; padding: 0 2px;';
  18. var TAG_COLORS = [
  19. // source: http://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12
  20. // background, foreground
  21. ['#a6cee3', 'black'],
  22. ['#b2df8a', 'black'],
  23. ['#fb9a99', 'black'],
  24. ['#fdbf6f', 'black'],
  25. ['#cab2d6', 'black'],
  26. ['#ffff99', 'black'],
  27. ['#1f78b4', 'white'],
  28. ['#33a02c', 'white'],
  29. ['#e31a1c', 'white'],
  30. ['#ff7f00', 'white'],
  31. ['#6a3d9a', 'white'],
  32. ['#b15928', 'white']
  33. ];
  34.  
  35. var tagsByFrequency;
  36.  
  37. var transIdToTags = {};
  38. var tagIdToName = {};
  39.  
  40. function maybeIngestTransactionsList(response) {
  41. var json = window.JSON.parse(response);
  42. json['set'].forEach(function(item) {
  43. if(item['id'] === 'transactions') {
  44. item['data'].forEach(function(trans) {
  45. transIdToTags[trans['id']] = trans['labels'].map(function(label) { return label['name']; });
  46. trans['labels'].forEach(function(label) {
  47. tagIdToName[label['id']] = label['name'];
  48. });
  49. });
  50. }
  51. });
  52. }
  53.  
  54. function maybeIngestTagsList(response) {
  55. var json = window.JSON.parse(response);
  56. if(json['bundleResponseSent']) {
  57. jQuery.each(json['response'], function(key, val) {
  58. if(val['responseType'] === 'MintTransactionService_getTagsByFrequency') {
  59. val['response'].forEach(function(tagData) {
  60. tagIdToName[tagData['id']] = tagData['name'];
  61. });
  62.  
  63. tagsByFrequency = val['response'].sort(function(a, b) {
  64. return b['transactionCount'] - a['transactionCount'];
  65. }).map(function(tagData) {
  66. return tagData['name'];
  67. });
  68. }
  69. });
  70. }
  71. }
  72.  
  73. function interceptTransactionEdit(data) {
  74. var transIds = [];
  75. var tagNames = [];
  76. data.split('&').forEach(function(pair) {
  77. var kv = pair.split('='), key = window.decodeURIComponent(kv[0]), val = window.decodeURIComponent(kv[1]);
  78.  
  79. var tagId = key.match(/tag(\d+)/);
  80. if(tagId !== null && val === '2') {
  81. tagNames.push(tagIdToName[tagId[1]]);
  82. }
  83.  
  84. // value is '1234:0' for a single transaction, '1234:0,2345:0' for multiple
  85. if(key === 'txnId') {
  86. transIds = val.split(',').map(function(tId) { return tId.split(':')[0]; });
  87. }
  88. });
  89.  
  90. transIds.forEach(function(tId) {
  91. transIdToTags[tId] = tagNames;
  92. if(jQuery('#transaction-' + tId).length > 0) {
  93. updateRow('transaction-' + tId);
  94. }
  95. });
  96. }
  97.  
  98. // update a transaction row using cached tag data
  99. function updateRow(rowId) {
  100. var $td = jQuery('#' + rowId).find('td.cat');
  101. var transId = rowId.split('-')[1];
  102. if(transIdToTags[transId] && transIdToTags[transId].length) {
  103. if($td.find('.gm-tags').length === 0) {
  104. $td.append('<span class="gm-tags" style="' + TAG_STYLE + '"></span>');
  105. }
  106.  
  107. // Alphabetize
  108. transIdToTags[transId].sort(function(a, b) {
  109. if(a.toLowerCase() < b.toLowerCase()) { return -1; }
  110. else if(a.toLowerCase() > b.toLowerCase()) { return 1; }
  111. else { return 0; }
  112. });
  113.  
  114. // HTML for each tag, unique color for each tag
  115. var tagsHTML = transIdToTags[transId].map(function(tag) {
  116. return '<span class="gm-tag" style="' + tagStyleLookup(tag) + '; ' + SINGLE_TAG_STYLE + '">' + tag + '</span>';
  117. }).join('');
  118.  
  119. $td.find('.gm-tags').html(tagsHTML);
  120.  
  121. } else {
  122. $td.find('.gm-tags').remove();
  123. }
  124. }
  125.  
  126. (function(open) {
  127. XMLHttpRequest.prototype.open = function() {
  128. // Firefox and Chrome support this.responseURL, but Safari does not, so we need to store it
  129. var requestURL_ = arguments[1];
  130.  
  131. // instrument all XHR responses to intercept the ones which may contain transaction listing or tag listing
  132. this.addEventListener("readystatechange", function() {
  133. if(this.readyState === 4 && requestURL_.match('getJsonData.xevent')) {
  134. maybeIngestTransactionsList(this.responseText);
  135. } else if(this.readyState === 4 && requestURL_.match('bundledServiceController.xevent')) {
  136. maybeIngestTagsList(this.responseText);
  137. }
  138. }, false);
  139.  
  140. // instrument all XHR requests to intercept edits to transactions
  141. if(arguments[0].match(/post/i) && arguments[1].match('updateTransaction.xevent')) {
  142. var self = this, send = this.send;
  143. this.send = function() {
  144. interceptTransactionEdit(arguments[0]);
  145. send.apply(self, arguments);
  146. };
  147. }
  148.  
  149. open.apply(this, arguments);
  150. };
  151. })(XMLHttpRequest.prototype.open);
  152.  
  153. function observeDOM(target) {
  154. var observer;
  155.  
  156. function handleMutations(mutations) {
  157. var rowIdsToUpdate = {};
  158. mutations.forEach(function(mutation) {
  159. var $target = jQuery(mutation.target);
  160. var $tr = jQuery(mutation.target).parents('tr').first();
  161. if(!$target.hasClass('gm-tags') && $tr.length && $tr.attr('id') && $tr.attr('id').indexOf('transaction-') === 0) {
  162. // when the transactions list changes, there will be multiple mutations per row (date column, amount column, etc.)
  163. rowIdsToUpdate[$tr.attr('id')] = true;
  164. }
  165. });
  166.  
  167. observer.disconnect();
  168. for(var rowId in rowIdsToUpdate) {
  169. updateRow(rowId);
  170. }
  171. observe();
  172. }
  173.  
  174. function observe() {
  175. observer = new MutationObserver(handleMutations);
  176. observer.observe(
  177. target,
  178. {subtree: true, childList: true, characterData: true}
  179. );
  180. }
  181.  
  182. observe();
  183. }
  184.  
  185. (function waitForTable() {
  186. var target = document.querySelector('#transaction-list-body');
  187. if(target === null) {
  188. setTimeout(waitForTable, 500);
  189. return;
  190. }
  191.  
  192. // populate the table with tags after it first loads
  193. jQuery(target).find('tr').each(function(_, row) {
  194. updateRow(row.id);
  195. });
  196.  
  197. observeDOM(target);
  198. })();
  199.  
  200. function tagStyleLookup(tag) {
  201. var index = tagsByFrequency.indexOf(tag);
  202. var colors = TAG_COLORS[index % TAG_COLORS.length];
  203. return 'background-color: ' + colors[0] + '; color: ' + colors[1] + ';';
  204. }
  205.  
  206. })();