data-validations-xform.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. const _ = require('../../../utils/under-dash');
  2. const utils = require('../../../utils/utils');
  3. const colCache = require('../../../utils/col-cache');
  4. const BaseXform = require('../base-xform');
  5. const Range = require('../../../doc/range');
  6. function assign(definedName, attributes, name, defaultValue) {
  7. const value = attributes[name];
  8. if (value !== undefined) {
  9. definedName[name] = value;
  10. } else if (defaultValue !== undefined) {
  11. definedName[name] = defaultValue;
  12. }
  13. }
  14. function assignBool(definedName, attributes, name, defaultValue) {
  15. const value = attributes[name];
  16. if (value !== undefined) {
  17. definedName[name] = utils.parseBoolean(value);
  18. } else if (defaultValue !== undefined) {
  19. definedName[name] = defaultValue;
  20. }
  21. }
  22. function optimiseDataValidations(model) {
  23. // Squeeze alike data validations together into rectangular ranges
  24. // to reduce file size and speed up Excel load time
  25. const dvList = _.map(model, (dataValidation, address) => ({
  26. address,
  27. dataValidation,
  28. marked: false,
  29. })).sort((a, b) => _.strcmp(a.address, b.address));
  30. const dvMap = _.keyBy(dvList, 'address');
  31. const matchCol = (addr, height, col) => {
  32. for (let i = 0; i < height; i++) {
  33. const otherAddress = colCache.encodeAddress(addr.row + i, col);
  34. if (!model[otherAddress] || !_.isEqual(model[addr.address], model[otherAddress])) {
  35. return false;
  36. }
  37. }
  38. return true;
  39. };
  40. return dvList
  41. .map(dv => {
  42. if (!dv.marked) {
  43. const addr = colCache.decodeEx(dv.address);
  44. if (addr.dimensions) {
  45. dvMap[addr.dimensions].marked = true;
  46. return {
  47. ...dv.dataValidation,
  48. sqref: dv.address,
  49. };
  50. }
  51. // iterate downwards - finding matching cells
  52. let height = 1;
  53. let otherAddress = colCache.encodeAddress(addr.row + height, addr.col);
  54. while (model[otherAddress] && _.isEqual(dv.dataValidation, model[otherAddress])) {
  55. height++;
  56. otherAddress = colCache.encodeAddress(addr.row + height, addr.col);
  57. }
  58. // iterate rightwards...
  59. let width = 1;
  60. while (matchCol(addr, height, addr.col + width)) {
  61. width++;
  62. }
  63. // mark all included addresses
  64. for (let i = 0; i < height; i++) {
  65. for (let j = 0; j < width; j++) {
  66. otherAddress = colCache.encodeAddress(addr.row + i, addr.col + j);
  67. dvMap[otherAddress].marked = true;
  68. }
  69. }
  70. if (height > 1 || width > 1) {
  71. const bottom = addr.row + (height - 1);
  72. const right = addr.col + (width - 1);
  73. return {
  74. ...dv.dataValidation,
  75. sqref: `${dv.address}:${colCache.encodeAddress(bottom, right)}`,
  76. };
  77. }
  78. return {
  79. ...dv.dataValidation,
  80. sqref: dv.address,
  81. };
  82. }
  83. return null;
  84. })
  85. .filter(Boolean);
  86. }
  87. class DataValidationsXform extends BaseXform {
  88. get tag() {
  89. return 'dataValidations';
  90. }
  91. render(xmlStream, model) {
  92. const optimizedModel = optimiseDataValidations(model);
  93. if (optimizedModel.length) {
  94. xmlStream.openNode('dataValidations', {count: optimizedModel.length});
  95. optimizedModel.forEach(value => {
  96. xmlStream.openNode('dataValidation');
  97. if (value.type !== 'any') {
  98. xmlStream.addAttribute('type', value.type);
  99. if (value.operator && value.type !== 'list' && value.operator !== 'between') {
  100. xmlStream.addAttribute('operator', value.operator);
  101. }
  102. if (value.allowBlank) {
  103. xmlStream.addAttribute('allowBlank', '1');
  104. }
  105. }
  106. if (value.showInputMessage) {
  107. xmlStream.addAttribute('showInputMessage', '1');
  108. }
  109. if (value.promptTitle) {
  110. xmlStream.addAttribute('promptTitle', value.promptTitle);
  111. }
  112. if (value.prompt) {
  113. xmlStream.addAttribute('prompt', value.prompt);
  114. }
  115. if (value.showErrorMessage) {
  116. xmlStream.addAttribute('showErrorMessage', '1');
  117. }
  118. if (value.errorStyle) {
  119. xmlStream.addAttribute('errorStyle', value.errorStyle);
  120. }
  121. if (value.errorTitle) {
  122. xmlStream.addAttribute('errorTitle', value.errorTitle);
  123. }
  124. if (value.error) {
  125. xmlStream.addAttribute('error', value.error);
  126. }
  127. xmlStream.addAttribute('sqref', value.sqref);
  128. (value.formulae || []).forEach((formula, index) => {
  129. xmlStream.openNode(`formula${index + 1}`);
  130. if (value.type === 'date') {
  131. xmlStream.writeText(utils.dateToExcel(new Date(formula)));
  132. } else {
  133. xmlStream.writeText(formula);
  134. }
  135. xmlStream.closeNode();
  136. });
  137. xmlStream.closeNode();
  138. });
  139. xmlStream.closeNode();
  140. }
  141. }
  142. parseOpen(node) {
  143. switch (node.name) {
  144. case 'dataValidations':
  145. this.model = {};
  146. return true;
  147. case 'dataValidation': {
  148. this._address = node.attributes.sqref;
  149. const dataValidation = {type: node.attributes.type || 'any', formulae: []};
  150. if (node.attributes.type) {
  151. assignBool(dataValidation, node.attributes, 'allowBlank');
  152. }
  153. assignBool(dataValidation, node.attributes, 'showInputMessage');
  154. assignBool(dataValidation, node.attributes, 'showErrorMessage');
  155. switch (dataValidation.type) {
  156. case 'any':
  157. case 'list':
  158. case 'custom':
  159. break;
  160. default:
  161. assign(dataValidation, node.attributes, 'operator', 'between');
  162. break;
  163. }
  164. assign(dataValidation, node.attributes, 'promptTitle');
  165. assign(dataValidation, node.attributes, 'prompt');
  166. assign(dataValidation, node.attributes, 'errorStyle');
  167. assign(dataValidation, node.attributes, 'errorTitle');
  168. assign(dataValidation, node.attributes, 'error');
  169. this._dataValidation = dataValidation;
  170. return true;
  171. }
  172. case 'formula1':
  173. case 'formula2':
  174. this._formula = [];
  175. return true;
  176. default:
  177. return false;
  178. }
  179. }
  180. parseText(text) {
  181. if (this._formula) {
  182. this._formula.push(text);
  183. }
  184. }
  185. parseClose(name) {
  186. switch (name) {
  187. case 'dataValidations':
  188. return false;
  189. case 'dataValidation': {
  190. if (!this._dataValidation.formulae || !this._dataValidation.formulae.length) {
  191. delete this._dataValidation.formulae;
  192. delete this._dataValidation.operator;
  193. }
  194. // The four known cases: 1. E4:L9 N4:U9 2.E4 L9 3. N4:U9 4. E4
  195. const list = this._address.split(/\s+/g) || [];
  196. list.forEach(addr => {
  197. if (addr.includes(':')) {
  198. const range = new Range(addr);
  199. range.forEachAddress(address => {
  200. this.model[address] = this._dataValidation;
  201. });
  202. } else {
  203. this.model[addr] = this._dataValidation;
  204. }
  205. });
  206. return true;
  207. }
  208. case 'formula1':
  209. case 'formula2': {
  210. let formula = this._formula.join('');
  211. switch (this._dataValidation.type) {
  212. case 'whole':
  213. case 'textLength':
  214. formula = parseInt(formula, 10);
  215. break;
  216. case 'decimal':
  217. formula = parseFloat(formula);
  218. break;
  219. case 'date':
  220. formula = utils.excelToDate(parseFloat(formula));
  221. break;
  222. default:
  223. break;
  224. }
  225. this._dataValidation.formulae.push(formula);
  226. this._formula = undefined;
  227. return true;
  228. }
  229. default:
  230. return true;
  231. }
  232. }
  233. }
  234. module.exports = DataValidationsXform;