handsontable.formula.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. (function (Handsontable) {
  2. 'use strict';
  3. function HandsontableFormula() {
  4. var formulaRenderer = function (instance, TD, row, col, prop, value, cellProperties) {
  5. if (instance.formulasEnabled) {
  6. // translate coordinates into cellId
  7. var cellId = instance.plugin.utils.translateCellCoords({row: row, col: col}),
  8. prevFormula = null,
  9. formula = null,
  10. needUpdate = false,
  11. error, result;
  12. if (!cellId) {
  13. return;
  14. }
  15. // get cell data
  16. var item = instance.plugin.matrix.getItem(cellId);
  17. if (item) {
  18. needUpdate = !!item.needUpdate;
  19. if (item.error) {
  20. prevFormula = item.formula;
  21. error = item.error;
  22. if (needUpdate) {
  23. error = null;
  24. }
  25. }
  26. }
  27. // check if typed formula or cell value should be recalculated
  28. if ((value && value[0] === '=') || needUpdate) {
  29. formula = value.substr(1).toUpperCase();
  30. if (!error || formula !== prevFormula) {
  31. var currentItem = item;
  32. if (!currentItem) {
  33. // define item to rulesJS matrix if not exists
  34. item = {
  35. id: cellId,
  36. formula: formula
  37. };
  38. // add item to matrix
  39. currentItem = instance.plugin.matrix.addItem(item);
  40. }
  41. // parse formula
  42. var newValue = instance.plugin.parse(formula, {row: row, col: col, id: cellId});
  43. // update item value and error
  44. instance.plugin.matrix.updateItem(currentItem, {formula: formula, value: newValue.result, error: newValue.error, needUpdate: false});
  45. error = newValue.error;
  46. result = newValue.result;
  47. // update cell value in hot
  48. value = error || result;
  49. }
  50. }
  51. if (error) {
  52. // clear cell value
  53. if (!value) {
  54. // reset error
  55. error = null;
  56. } else {
  57. // show error
  58. value = error;
  59. }
  60. }
  61. // change background color
  62. if (instance.plugin.utils.isSet(error)) {
  63. Handsontable.Dom.addClass(TD, 'formula-error');
  64. } else if (instance.plugin.utils.isSet(result)) {
  65. Handsontable.Dom.removeClass(TD, 'formula-error');
  66. Handsontable.Dom.addClass(TD, 'formula');
  67. }
  68. }
  69. // apply changes
  70. textCell.renderer.apply(this, [instance, TD, row, col, prop, value, cellProperties]);
  71. };
  72. var afterChange = function (changes, source) {
  73. var instance = this;
  74. if (!instance.formulasEnabled) {
  75. return;
  76. }
  77. if (source === 'edit' || source === 'undo' || source === 'autofill') {
  78. var rerender = false;
  79. changes.forEach(function (item) {
  80. var row = item[0],
  81. col = item[1],
  82. prevValue = item[2],
  83. value = item[3];
  84. var cellId = instance.plugin.utils.translateCellCoords({row: row, col: col});
  85. // if changed value, all references cells should be recalculated
  86. if (value[0] !== '=' || prevValue !== value) {
  87. instance.plugin.matrix.removeItem(cellId);
  88. // get referenced cells
  89. var deps = instance.plugin.matrix.getDependencies(cellId);
  90. // update cells
  91. deps.forEach(function (itemId) {
  92. instance.plugin.matrix.updateItem(itemId, {needUpdate: true});
  93. });
  94. rerender = true;
  95. }
  96. });
  97. if (rerender) {
  98. instance.render();
  99. }
  100. }
  101. };
  102. var beforeAutofillInsidePopulate = function (index, direction, data, deltas, iterators, selected) {
  103. var instance = this;
  104. var r = index.row,
  105. c = index.col,
  106. value = data[r][c],
  107. delta = 0,
  108. rlength = data.length, // rows
  109. clength = data ? data[0].length : 0; //cols
  110. if (value[0] === '=') { // formula
  111. if (['down', 'up'].indexOf(direction) !== -1) {
  112. delta = rlength * iterators.row;
  113. } else if (['right', 'left'].indexOf(direction) !== -1) {
  114. delta = clength * iterators.col;
  115. }
  116. return {
  117. value: instance.plugin.utils.updateFormula(value, direction, delta),
  118. iterators: iterators
  119. }
  120. } else { // other value
  121. // increment or decrement values for more than 2 selected cells
  122. if (rlength >= 2 || clength >= 2) {
  123. var newValue = instance.plugin.helper.number(value),
  124. ii,
  125. start;
  126. if (instance.plugin.utils.isNumber(newValue)) {
  127. if (['down', 'up'].indexOf(direction) !== -1) {
  128. delta = deltas[0][c];
  129. if (direction === 'down') {
  130. newValue += (delta * rlength * iterators.row);
  131. } else {
  132. ii = (selected.row - r) % rlength;
  133. start = ii > 0 ? rlength - ii : 0;
  134. newValue = instance.plugin.helper.number(data[start][c]);
  135. newValue += (delta * rlength * iterators.row);
  136. // last element in array -> decrement iterator
  137. // iterator cannot be less than 1
  138. if (iterators.row > 1 && (start + 1) === rlength) {
  139. iterators.row--;
  140. }
  141. }
  142. } else if (['right', 'left'].indexOf(direction) !== -1) {
  143. delta = deltas[r][0];
  144. if (direction === 'right') {
  145. newValue += (delta * clength * iterators.col);
  146. } else {
  147. ii = (selected.col - c) % clength;
  148. start = ii > 0 ? clength - ii : 0;
  149. newValue = instance.plugin.helper.number(data[r][start]);
  150. newValue += (delta * clength * (iterators.col || 1));
  151. // last element in array -> decrement iterator
  152. // iterator cannot be less than 1
  153. if (iterators.col > 1 && (start + 1) === clength) {
  154. iterators.col--;
  155. }
  156. }
  157. }
  158. return {
  159. value: newValue,
  160. iterators: iterators
  161. }
  162. }
  163. }
  164. }
  165. return {
  166. value: value,
  167. iterators: iterators
  168. };
  169. };
  170. var afterCreateRow = function (row, amount, auto) {
  171. if (auto) {
  172. return;
  173. }
  174. var instance = this;
  175. var selectedRow = instance.plugin.utils.isArray(instance.getSelected()) ? instance.getSelected()[0] : undefined;
  176. if (instance.plugin.utils.isUndefined(selectedRow)) {
  177. return;
  178. }
  179. var direction = (selectedRow >= row) ? 'before' : 'after',
  180. items = instance.plugin.matrix.getRefItemsToRow(row),
  181. counter = 1,
  182. changes = [];
  183. items.forEach(function (id) {
  184. var item = instance.plugin.matrix.getItem(id),
  185. formula = instance.plugin.utils.changeFormula(item.formula, 1, {row: row}), // update formula if needed
  186. newId = id;
  187. if (formula !== item.formula) { // formula updated
  188. // change row index and get new coordinates
  189. if ((direction === 'before' && selectedRow <= item.row) || (direction === 'after' && selectedRow < item.row)) {
  190. newId = instance.plugin.utils.changeRowIndex(id, counter);
  191. }
  192. var cellCoords = instance.plugin.utils.cellCoords(newId);
  193. if (newId !== id) {
  194. // remove current item from matrix
  195. instance.plugin.matrix.removeItem(id);
  196. }
  197. // set updated formula in new cell
  198. changes.push([cellCoords.row, cellCoords.col, '=' + formula]);
  199. }
  200. });
  201. if (items) {
  202. instance.plugin.matrix.removeItemsBelowRow(row);
  203. }
  204. if (changes) {
  205. instance.setDataAtCell(changes);
  206. }
  207. };
  208. var afterCreateCol = function (col) {
  209. var instance = this;
  210. var selectedCol = instance.plugin.utils.isArray(instance.getSelected()) ? instance.getSelected()[1] : undefined;
  211. if (instance.plugin.utils.isUndefined(selectedCol)) {
  212. return;
  213. }
  214. var items = instance.plugin.matrix.getRefItemsToColumn(col),
  215. counter = 1,
  216. direction = (selectedCol >= col) ? 'before' : 'after',
  217. changes = [];
  218. items.forEach(function (id) {
  219. var item = instance.plugin.matrix.getItem(id),
  220. formula = instance.plugin.utils.changeFormula(item.formula, 1, {col: col}), // update formula if needed
  221. newId = id;
  222. if (formula !== item.formula) { // formula updated
  223. // change col index and get new coordinates
  224. if ((direction === 'before' && selectedCol <= item.col) || (direction === 'after' && selectedCol < item.col)) {
  225. newId = instance.plugin.utils.changeColIndex(id, counter);
  226. }
  227. var cellCoords = instance.plugin.utils.cellCoords(newId);
  228. if (newId !== id) {
  229. // remove current item from matrix if id changed
  230. instance.plugin.matrix.removeItem(id);
  231. }
  232. // set updated formula in new cell
  233. changes.push([cellCoords.row, cellCoords.col, '=' + formula]);
  234. }
  235. });
  236. if (items) {
  237. instance.plugin.matrix.removeItemsBelowCol(col);
  238. }
  239. if (changes) {
  240. instance.setDataAtCell(changes);
  241. }
  242. };
  243. var formulaCell = {
  244. renderer: formulaRenderer,
  245. editor: Handsontable.editors.TextEditor,
  246. dataType: 'formula'
  247. };
  248. var textCell = {
  249. renderer: Handsontable.renderers.TextRenderer,
  250. editor: Handsontable.editors.TextEditor
  251. };
  252. this.init = function () {
  253. var instance = this;
  254. instance.formulasEnabled = !!instance.getSettings().formulas;
  255. if (instance.formulasEnabled) {
  256. var custom = {
  257. cellValue: instance.getDataAtCell
  258. };
  259. instance.plugin = new ruleJS();
  260. instance.plugin.init();
  261. instance.plugin.custom = custom;
  262. Handsontable.cellTypes['formula'] = formulaCell;
  263. Handsontable.TextCell.renderer = formulaRenderer;
  264. instance.addHook('afterChange', afterChange);
  265. instance.addHook('beforeAutofillInsidePopulate', beforeAutofillInsidePopulate);
  266. instance.addHook('afterCreateRow', afterCreateRow);
  267. instance.addHook('afterCreateCol', afterCreateCol);
  268. } else {
  269. instance.removeHook('afterChange', afterChange);
  270. instance.removeHook('beforeAutofillInsidePopulate', beforeAutofillInsidePopulate);
  271. instance.removeHook('afterCreateRow', afterCreateRow);
  272. instance.removeHook('afterCreateCol', afterCreateCol);
  273. }
  274. };
  275. }
  276. var htFormula = new HandsontableFormula();
  277. Handsontable.hooks.add('beforeInit', htFormula.init);
  278. Handsontable.hooks.add('afterUpdateSettings', function () {
  279. htFormula.init.call(this, 'afterUpdateSettings')
  280. });
  281. })(Handsontable);