dataMap.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  1. import SheetClip from './../lib/SheetClip/SheetClip';
  2. import { cellMethodLookupFactory } from './helpers/data';
  3. import { columnFactory } from './helpers/setting';
  4. import {
  5. createObjectPropListener,
  6. deepClone,
  7. deepExtend,
  8. deepObjectSize,
  9. duckSchema,
  10. hasOwnProperty,
  11. isObject,
  12. objectEach
  13. } from './helpers/object';
  14. import { extendArray, to2dArray } from './helpers/array';
  15. import Interval from './utils/interval';
  16. import { rangeEach } from './helpers/number';
  17. import MultiMap from './multiMap';
  18. /**
  19. * Utility class that gets and saves data from/to the data source using mapping of columns numbers to object property names
  20. * @todo refactor arguments of methods getRange, getText to be numbers (not objects)
  21. * @todo remove priv, GridSettings from object constructor
  22. *
  23. * @param {Object} instance Instance of Handsontable
  24. * @param {*} priv
  25. * @param {*} GridSettings Grid settings
  26. * @util
  27. * @class DataMap
  28. */
  29. function DataMap(instance, priv, GridSettings) {
  30. this.instance = instance;
  31. this.priv = priv;
  32. this.GridSettings = GridSettings;
  33. this.dataSource = this.instance.getSettings().data;
  34. this.cachedLength = null;
  35. this.skipCache = false;
  36. this.latestSourceRowsCount = 0;
  37. if (this.dataSource && this.dataSource[0]) {
  38. this.duckSchema = this.recursiveDuckSchema(this.dataSource[0]);
  39. } else {
  40. this.duckSchema = {};
  41. }
  42. this.createMap();
  43. this.interval = Interval.create(() => this.clearLengthCache(), '15fps');
  44. this.instance.addHook('skipLengthCache', delay => this.onSkipLengthCache(delay));
  45. this.onSkipLengthCache(500);
  46. }
  47. DataMap.prototype.DESTINATION_RENDERER = 1;
  48. DataMap.prototype.DESTINATION_CLIPBOARD_GENERATOR = 2;
  49. /**
  50. * @param {Object|Array} object
  51. * @returns {Object|Array}
  52. */
  53. DataMap.prototype.recursiveDuckSchema = function(object) {
  54. return duckSchema(object);
  55. };
  56. /**
  57. * @param {Object} schema
  58. * @param {Number} lastCol
  59. * @param {Number} parent
  60. * @returns {Number}
  61. */
  62. DataMap.prototype.recursiveDuckColumns = function(schema, lastCol, parent) {
  63. let lastColumn = lastCol;
  64. let propertyParent = parent;
  65. let prop;
  66. if (typeof lastColumn === 'undefined') {
  67. lastColumn = 0;
  68. propertyParent = '';
  69. }
  70. if (typeof schema === 'object' && !Array.isArray(schema)) {
  71. objectEach(schema, (value, key) => {
  72. if (value === null) {
  73. prop = propertyParent + key;
  74. this.colToPropCache.push(prop);
  75. this.propToColCache.set(prop, lastColumn);
  76. lastColumn += 1;
  77. } else {
  78. lastColumn = this.recursiveDuckColumns(value, lastColumn, `${key}.`);
  79. }
  80. });
  81. }
  82. return lastColumn;
  83. };
  84. DataMap.prototype.createMap = function() {
  85. let i;
  86. const schema = this.getSchema();
  87. if (typeof schema === 'undefined') {
  88. throw new Error('trying to create `columns` definition but you didn\'t provide `schema` nor `data`');
  89. }
  90. this.colToPropCache = [];
  91. this.propToColCache = new MultiMap();
  92. const columns = this.instance.getSettings().columns;
  93. if (columns) {
  94. const maxCols = this.instance.getSettings().maxCols;
  95. let columnsLen = Math.min(maxCols, columns.length);
  96. let filteredIndex = 0;
  97. let columnsAsFunc = false;
  98. const schemaLen = deepObjectSize(schema);
  99. if (typeof columns === 'function') {
  100. columnsLen = schemaLen > 0 ? schemaLen : this.instance.countSourceCols();
  101. columnsAsFunc = true;
  102. }
  103. for (i = 0; i < columnsLen; i++) {
  104. const column = columnsAsFunc ? columns(i) : columns[i];
  105. if (isObject(column)) {
  106. if (typeof column.data !== 'undefined') {
  107. const index = columnsAsFunc ? filteredIndex : i;
  108. this.colToPropCache[index] = column.data;
  109. this.propToColCache.set(column.data, index);
  110. }
  111. filteredIndex += 1;
  112. }
  113. }
  114. } else {
  115. this.recursiveDuckColumns(schema);
  116. }
  117. };
  118. /**
  119. * Returns property name that corresponds with the given column index.
  120. *
  121. * @param {Number} col Visual column index.
  122. * @returns {Number} Physical column index.
  123. */
  124. DataMap.prototype.colToProp = function(col) {
  125. const physicalColumn = this.instance.runHooks('modifyCol', col);
  126. if (!isNaN(physicalColumn) && this.colToPropCache && typeof this.colToPropCache[physicalColumn] !== 'undefined') {
  127. return this.colToPropCache[physicalColumn];
  128. }
  129. return physicalColumn;
  130. };
  131. /**
  132. * @param {Object} prop
  133. * @fires Hooks#modifyCol
  134. * @returns {*}
  135. */
  136. DataMap.prototype.propToCol = function(prop) {
  137. let col;
  138. if (typeof this.propToColCache.get(prop) === 'undefined') {
  139. col = prop;
  140. } else {
  141. col = this.propToColCache.get(prop);
  142. }
  143. col = this.instance.runHooks('unmodifyCol', col);
  144. return col;
  145. };
  146. /**
  147. * @returns {Object}
  148. */
  149. DataMap.prototype.getSchema = function() {
  150. const schema = this.instance.getSettings().dataSchema;
  151. if (schema) {
  152. if (typeof schema === 'function') {
  153. return schema();
  154. }
  155. return schema;
  156. }
  157. return this.duckSchema;
  158. };
  159. /**
  160. * Creates row at the bottom of the data array.
  161. *
  162. * @param {Number} [index] Physical index of the row before which the new row will be inserted.
  163. * @param {Number} [amount=1] An amount of rows to add.
  164. * @param {String} [source] Source of method call.
  165. * @fires Hooks#afterCreateRow
  166. * @returns {Number} Returns number of created rows.
  167. */
  168. DataMap.prototype.createRow = function(index, amount = 1, source) {
  169. let numberOfCreatedRows = 0;
  170. let rowIndex = index;
  171. if (typeof rowIndex !== 'number' || rowIndex >= this.instance.countSourceRows()) {
  172. rowIndex = this.instance.countSourceRows();
  173. }
  174. const continueProcess = this.instance.runHooks('beforeCreateRow', rowIndex, amount, source);
  175. if (continueProcess === false) {
  176. return 0;
  177. }
  178. const maxRows = this.instance.getSettings().maxRows;
  179. const columnCount = this.instance.countCols();
  180. while (numberOfCreatedRows < amount && this.instance.countSourceRows() < maxRows) {
  181. let row = null;
  182. if (this.instance.dataType === 'array') {
  183. if (this.instance.getSettings().dataSchema) {
  184. // Clone template array
  185. row = deepClone(this.getSchema());
  186. } else {
  187. row = [];
  188. /* eslint-disable no-loop-func */
  189. rangeEach(columnCount - 1, () => row.push(null));
  190. }
  191. } else if (this.instance.dataType === 'function') {
  192. row = this.instance.getSettings().dataSchema(rowIndex);
  193. } else {
  194. row = {};
  195. deepExtend(row, this.getSchema());
  196. }
  197. if (rowIndex === this.instance.countSourceRows()) {
  198. this.dataSource.push(row);
  199. } else {
  200. this.spliceData(rowIndex, 0, row);
  201. }
  202. numberOfCreatedRows += 1;
  203. }
  204. this.instance.runHooks('afterCreateRow', rowIndex, numberOfCreatedRows, source);
  205. this.instance.forceFullRender = true; // used when data was changed
  206. return numberOfCreatedRows;
  207. };
  208. /**
  209. * Creates col at the right of the data array.
  210. *
  211. * @param {Number} [index] Visual index of the column before which the new column will be inserted
  212. * @param {Number} [amount=1] An amount of columns to add.
  213. * @param {String} [source] Source of method call.
  214. * @fires Hooks#afterCreateCol
  215. * @returns {Number} Returns number of created columns
  216. */
  217. DataMap.prototype.createCol = function(index, amount = 1, source) {
  218. if (!this.instance.isColumnModificationAllowed()) {
  219. throw new Error('Cannot create new column. When data source in an object, ' +
  220. 'you can only have as much columns as defined in first data row, data schema or in the \'columns\' setting.' +
  221. 'If you want to be able to add new columns, you have to use array datasource.');
  222. }
  223. const rlen = this.instance.countSourceRows();
  224. const data = this.dataSource;
  225. const countColumns = this.instance.countCols();
  226. const columnIndex = typeof index !== 'number' || index >= countColumns ? countColumns : index;
  227. let constructor;
  228. let numberOfCreatedCols = 0;
  229. let currentIndex;
  230. this.instance.runHooks('beforeCreateCol', columnIndex, amount, source);
  231. currentIndex = columnIndex;
  232. const maxCols = this.instance.getSettings().maxCols;
  233. while (numberOfCreatedCols < amount && this.instance.countCols() < maxCols) {
  234. constructor = columnFactory(this.GridSettings, this.priv.columnsSettingConflicts);
  235. if (typeof columnIndex !== 'number' || columnIndex >= this.instance.countCols()) {
  236. if (rlen > 0) {
  237. for (let r = 0; r < rlen; r++) {
  238. if (typeof data[r] === 'undefined') {
  239. data[r] = [];
  240. }
  241. data[r].push(null);
  242. }
  243. } else {
  244. data.push([null]);
  245. }
  246. // Add new column constructor
  247. this.priv.columnSettings.push(constructor);
  248. } else {
  249. for (let row = 0; row < rlen; row++) {
  250. data[row].splice(currentIndex, 0, null);
  251. }
  252. // Add new column constructor at given index
  253. this.priv.columnSettings.splice(currentIndex, 0, constructor);
  254. }
  255. numberOfCreatedCols += 1;
  256. currentIndex += 1;
  257. }
  258. this.instance.runHooks('afterCreateCol', columnIndex, numberOfCreatedCols, source);
  259. this.instance.forceFullRender = true; // used when data was changed
  260. return numberOfCreatedCols;
  261. };
  262. /**
  263. * Removes row from the data array.
  264. *
  265. * @param {Number} [index] Visual index of the row to be removed. If not provided, the last row will be removed
  266. * @param {Number} [amount=1] Amount of the rows to be removed. If not provided, one row will be removed
  267. * @param {String} [source] Source of method call.
  268. * @fires Hooks#beforeRemoveRow
  269. * @fires Hooks#afterRemoveRow
  270. */
  271. DataMap.prototype.removeRow = function(index, amount = 1, source) {
  272. let rowIndex = typeof index !== 'number' ? -amount : index;
  273. const rowsAmount = this.instance.runHooks('modifyRemovedAmount', amount, rowIndex);
  274. rowIndex = (this.instance.countSourceRows() + rowIndex) % this.instance.countSourceRows();
  275. const logicRows = this.visualRowsToPhysical(rowIndex, rowsAmount);
  276. const actionWasNotCancelled = this.instance.runHooks('beforeRemoveRow', rowIndex, rowsAmount, logicRows, source);
  277. if (actionWasNotCancelled === false) {
  278. return;
  279. }
  280. const data = this.dataSource;
  281. const newData = this.filterData(rowIndex, rowsAmount);
  282. if (newData) {
  283. data.length = 0;
  284. Array.prototype.push.apply(data, newData);
  285. }
  286. this.instance.runHooks('afterRemoveRow', rowIndex, rowsAmount, logicRows, source);
  287. this.instance.forceFullRender = true; // used when data was changed
  288. };
  289. /**
  290. * Removes column from the data array.
  291. *
  292. * @param {Number} [index] Visual index of the column to be removed. If not provided, the last column will be removed
  293. * @param {Number} [amount=1] Amount of the columns to be removed. If not provided, one column will be removed
  294. * @param {String} [source] Source of method call.
  295. * @fires Hooks#beforeRemoveCol
  296. * @fires Hooks#afterRemoveCol
  297. */
  298. DataMap.prototype.removeCol = function(index, amount = 1, source) {
  299. if (this.instance.dataType === 'object' || this.instance.getSettings().columns) {
  300. throw new Error('cannot remove column with object data source or columns option specified');
  301. }
  302. let columnIndex = typeof index !== 'number' ? -amount : index;
  303. columnIndex = (this.instance.countCols() + columnIndex) % this.instance.countCols();
  304. const logicColumns = this.visualColumnsToPhysical(columnIndex, amount);
  305. const descendingLogicColumns = logicColumns.slice(0).sort((a, b) => b - a);
  306. const actionWasNotCancelled = this.instance.runHooks('beforeRemoveCol', columnIndex, amount, logicColumns, source);
  307. if (actionWasNotCancelled === false) {
  308. return;
  309. }
  310. let isTableUniform = true;
  311. const removedColumnsCount = descendingLogicColumns.length;
  312. const data = this.dataSource;
  313. for (let c = 0; c < removedColumnsCount; c++) {
  314. if (isTableUniform && logicColumns[0] !== logicColumns[c] - c) {
  315. isTableUniform = false;
  316. }
  317. }
  318. if (isTableUniform) {
  319. for (let r = 0, rlen = this.instance.countSourceRows(); r < rlen; r++) {
  320. data[r].splice(logicColumns[0], amount);
  321. }
  322. } else {
  323. for (let r = 0, rlen = this.instance.countSourceRows(); r < rlen; r++) {
  324. for (let c = 0; c < removedColumnsCount; c++) {
  325. data[r].splice(descendingLogicColumns[c], 1);
  326. }
  327. }
  328. for (let c = 0; c < removedColumnsCount; c++) {
  329. this.priv.columnSettings.splice(logicColumns[c], 1);
  330. }
  331. }
  332. this.instance.runHooks('afterRemoveCol', columnIndex, amount, logicColumns, source);
  333. this.instance.forceFullRender = true; // used when data was changed
  334. };
  335. /**
  336. * Add/Removes data from the column.
  337. *
  338. * @param {Number} col Physical index of column in which do you want to do splice
  339. * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end
  340. * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed
  341. * @param {Array} [elements]
  342. * @returns {Array} Returns removed portion of columns
  343. */
  344. DataMap.prototype.spliceCol = function(col, index, amount, ...elements) {
  345. const colData = this.instance.getDataAtCol(col);
  346. const removed = colData.slice(index, index + amount);
  347. const after = colData.slice(index + amount);
  348. extendArray(elements, after);
  349. let i = 0;
  350. while (i < amount) {
  351. elements.push(null); // add null in place of removed elements
  352. i += 1;
  353. }
  354. to2dArray(elements);
  355. this.instance.populateFromArray(index, col, elements, null, null, 'spliceCol');
  356. return removed;
  357. };
  358. /**
  359. * Add/Removes data from the row.
  360. *
  361. * @param {Number} row Physical index of row in which do you want to do splice
  362. * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end.
  363. * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed.
  364. * @param {Array} [elements]
  365. * @returns {Array} Returns removed portion of rows
  366. */
  367. DataMap.prototype.spliceRow = function(row, index, amount, ...elements) {
  368. const rowData = this.instance.getSourceDataAtRow(row);
  369. const removed = rowData.slice(index, index + amount);
  370. const after = rowData.slice(index + amount);
  371. extendArray(elements, after);
  372. let i = 0;
  373. while (i < amount) {
  374. elements.push(null); // add null in place of removed elements
  375. i += 1;
  376. }
  377. this.instance.populateFromArray(row, index, [elements], null, null, 'spliceRow');
  378. return removed;
  379. };
  380. /**
  381. * Add/remove row(s) to/from the data source.
  382. *
  383. * @param {Number} index Physical index of the element to remove.
  384. * @param {Number} amount Number of rows to add/remove.
  385. * @param {Object} element Row to add.
  386. */
  387. DataMap.prototype.spliceData = function(index, amount, element) {
  388. const continueSplicing = this.instance.runHooks('beforeDataSplice', index, amount, element);
  389. if (continueSplicing !== false) {
  390. this.dataSource.splice(index, amount, element);
  391. }
  392. };
  393. /**
  394. * Filter unwanted data elements from the data source.
  395. *
  396. * @param {Number} index Visual index of the element to remove.
  397. * @param {Number} amount Number of rows to add/remove.
  398. * @returns {Array}
  399. */
  400. DataMap.prototype.filterData = function(index, amount) {
  401. const physicalRows = this.visualRowsToPhysical(index, amount);
  402. const continueSplicing = this.instance.runHooks('beforeDataFilter', index, amount, physicalRows);
  403. if (continueSplicing !== false) {
  404. const newData = this.dataSource.filter((row, rowIndex) => physicalRows.indexOf(rowIndex) === -1);
  405. return newData;
  406. }
  407. };
  408. /**
  409. * Returns single value from the data array.
  410. *
  411. * @param {Number} row Visual row index.
  412. * @param {Number} prop
  413. */
  414. DataMap.prototype.get = function(row, prop) {
  415. const physicalRow = this.instance.runHooks('modifyRow', row);
  416. let dataRow = this.dataSource[physicalRow];
  417. // TODO: To remove, use 'modifyData' hook instead (see below)
  418. const modifiedRowData = this.instance.runHooks('modifyRowData', physicalRow);
  419. dataRow = isNaN(modifiedRowData) ? modifiedRowData : dataRow;
  420. //
  421. let value = null;
  422. // try to get value under property `prop` (includes dot)
  423. if (dataRow && dataRow.hasOwnProperty && hasOwnProperty(dataRow, prop)) {
  424. value = dataRow[prop];
  425. } else if (typeof prop === 'string' && prop.indexOf('.') > -1) {
  426. const sliced = prop.split('.');
  427. let out = dataRow;
  428. if (!out) {
  429. return null;
  430. }
  431. for (let i = 0, ilen = sliced.length; i < ilen; i++) {
  432. out = out[sliced[i]];
  433. if (typeof out === 'undefined' || out === null) {
  434. return null;
  435. }
  436. }
  437. value = out;
  438. } else if (typeof prop === 'function') {
  439. /**
  440. * allows for interacting with complex structures, for example
  441. * d3/jQuery getter/setter properties:
  442. *
  443. * {columns: [{
  444. * data: function(row, value){
  445. * if(arguments.length === 1){
  446. * return row.property();
  447. * }
  448. * row.property(value);
  449. * }
  450. * }]}
  451. */
  452. value = prop(this.dataSource.slice(physicalRow, physicalRow + 1)[0]);
  453. }
  454. if (this.instance.hasHook('modifyData')) {
  455. const valueHolder = createObjectPropListener(value);
  456. this.instance.runHooks('modifyData', physicalRow, this.propToCol(prop), valueHolder, 'get');
  457. if (valueHolder.isTouched()) {
  458. value = valueHolder.value;
  459. }
  460. }
  461. return value;
  462. };
  463. const copyableLookup = cellMethodLookupFactory('copyable', false);
  464. /**
  465. * Returns single value from the data array (intended for clipboard copy to an external application).
  466. *
  467. * @param {Number} row Physical row index.
  468. * @param {Number} prop
  469. * @returns {String}
  470. */
  471. DataMap.prototype.getCopyable = function(row, prop) {
  472. if (copyableLookup.call(this.instance, row, this.propToCol(prop))) {
  473. return this.get(row, prop);
  474. }
  475. return '';
  476. };
  477. /**
  478. * Saves single value to the data array.
  479. *
  480. * @param {Number} row Visual row index.
  481. * @param {Number} prop
  482. * @param {String} value
  483. * @param {String} [source] Source of hook runner.
  484. */
  485. DataMap.prototype.set = function(row, prop, value, source) {
  486. const physicalRow = this.instance.runHooks('modifyRow', row, source || 'datamapGet');
  487. let newValue = value;
  488. let dataRow = this.dataSource[physicalRow];
  489. // TODO: To remove, use 'modifyData' hook instead (see below)
  490. const modifiedRowData = this.instance.runHooks('modifyRowData', physicalRow);
  491. dataRow = isNaN(modifiedRowData) ? modifiedRowData : dataRow;
  492. //
  493. if (this.instance.hasHook('modifyData')) {
  494. const valueHolder = createObjectPropListener(newValue);
  495. this.instance.runHooks('modifyData', physicalRow, this.propToCol(prop), valueHolder, 'set');
  496. if (valueHolder.isTouched()) {
  497. newValue = valueHolder.value;
  498. }
  499. }
  500. // try to set value under property `prop` (includes dot)
  501. if (dataRow && dataRow.hasOwnProperty && hasOwnProperty(dataRow, prop)) {
  502. dataRow[prop] = newValue;
  503. } else if (typeof prop === 'string' && prop.indexOf('.') > -1) {
  504. const sliced = prop.split('.');
  505. let out = dataRow;
  506. let i = 0;
  507. let ilen;
  508. for (i = 0, ilen = sliced.length - 1; i < ilen; i++) {
  509. if (typeof out[sliced[i]] === 'undefined') {
  510. out[sliced[i]] = {};
  511. }
  512. out = out[sliced[i]];
  513. }
  514. out[sliced[i]] = newValue;
  515. } else if (typeof prop === 'function') {
  516. /* see the `function` handler in `get` */
  517. prop(this.dataSource.slice(physicalRow, physicalRow + 1)[0], newValue);
  518. } else {
  519. dataRow[prop] = newValue;
  520. }
  521. };
  522. /**
  523. * This ridiculous piece of code maps rows Id that are present in table data to those displayed for user.
  524. * The trick is, the physical row id (stored in settings.data) is not necessary the same
  525. * as the visual (displayed) row id (e.g. when sorting is applied).
  526. *
  527. * @param {Number} index Visual row index.
  528. * @param {Number} amount
  529. * @fires Hooks#modifyRow
  530. * @returns {Number}
  531. */
  532. DataMap.prototype.visualRowsToPhysical = function(index, amount) {
  533. const totalRows = this.instance.countSourceRows();
  534. const logicRows = [];
  535. let physicRow = (totalRows + index) % totalRows;
  536. let rowsToRemove = amount;
  537. let row;
  538. while (physicRow < totalRows && rowsToRemove) {
  539. row = this.instance.runHooks('modifyRow', physicRow);
  540. logicRows.push(row);
  541. rowsToRemove -= 1;
  542. physicRow += 1;
  543. }
  544. return logicRows;
  545. };
  546. /**
  547. *
  548. * @param index Visual column index.
  549. * @param amount
  550. * @returns {Array}
  551. */
  552. DataMap.prototype.visualColumnsToPhysical = function(index, amount) {
  553. const totalCols = this.instance.countCols();
  554. let physicalCol = (totalCols + index) % totalCols;
  555. const visualCols = [];
  556. let colsToRemove = amount;
  557. while (physicalCol < totalCols && colsToRemove) {
  558. const col = this.instance.runHooks('modifyCol', physicalCol);
  559. visualCols.push(col);
  560. colsToRemove -= 1;
  561. physicalCol += 1;
  562. }
  563. return visualCols;
  564. };
  565. /**
  566. * Clears the data array.
  567. */
  568. DataMap.prototype.clear = function() {
  569. for (let r = 0; r < this.instance.countSourceRows(); r++) {
  570. for (let c = 0; c < this.instance.countCols(); c++) {
  571. this.set(r, this.colToProp(c), '');
  572. }
  573. }
  574. };
  575. /**
  576. * Clear cached data length.
  577. */
  578. DataMap.prototype.clearLengthCache = function() {
  579. this.cachedLength = null;
  580. };
  581. /**
  582. * Get data length.
  583. *
  584. * @returns {Number}
  585. */
  586. DataMap.prototype.getLength = function() {
  587. const maxRowsFromSettings = this.instance.getSettings().maxRows;
  588. let maxRows;
  589. if (maxRowsFromSettings < 0 || maxRowsFromSettings === 0) {
  590. maxRows = 0;
  591. } else {
  592. maxRows = maxRowsFromSettings || Infinity;
  593. }
  594. let length = this.instance.countSourceRows();
  595. if (this.instance.hasHook('modifyRow')) {
  596. let reValidate = this.skipCache;
  597. this.interval.start();
  598. if (length !== this.latestSourceRowsCount) {
  599. reValidate = true;
  600. }
  601. this.latestSourceRowsCount = length;
  602. if (this.cachedLength === null || reValidate) {
  603. rangeEach(length - 1, (row) => {
  604. const physicalRow = this.instance.runHooks('modifyRow', row);
  605. if (physicalRow === null) {
  606. length -= 1;
  607. }
  608. });
  609. this.cachedLength = length;
  610. } else {
  611. length = this.cachedLength;
  612. }
  613. } else {
  614. this.interval.stop();
  615. }
  616. return Math.min(length, maxRows);
  617. };
  618. /**
  619. * Returns the data array.
  620. *
  621. * @returns {Array}
  622. */
  623. DataMap.prototype.getAll = function() {
  624. const start = {
  625. row: 0,
  626. col: 0,
  627. };
  628. const end = {
  629. row: Math.max(this.instance.countSourceRows() - 1, 0),
  630. col: Math.max(this.instance.countCols() - 1, 0),
  631. };
  632. if (start.row - end.row === 0 && !this.instance.countSourceRows()) {
  633. return [];
  634. }
  635. return this.getRange(start, end, DataMap.prototype.DESTINATION_RENDERER);
  636. };
  637. /**
  638. * Returns data range as array.
  639. *
  640. * @param {Object} [start] Start selection position. Visual indexes.
  641. * @param {Object} [end] End selection position. Visual indexes.
  642. * @param {Number} destination Destination of datamap.get
  643. * @returns {Array}
  644. */
  645. DataMap.prototype.getRange = function(start, end, destination) {
  646. const output = [];
  647. let r;
  648. let c;
  649. let row;
  650. const maxRows = this.instance.getSettings().maxRows;
  651. const maxCols = this.instance.getSettings().maxCols;
  652. if (maxRows === 0 || maxCols === 0) {
  653. return [];
  654. }
  655. const getFn = destination === this.DESTINATION_CLIPBOARD_GENERATOR ? this.getCopyable : this.get;
  656. const rlen = Math.min(Math.max(maxRows - 1, 0), Math.max(start.row, end.row));
  657. const clen = Math.min(Math.max(maxCols - 1, 0), Math.max(start.col, end.col));
  658. for (r = Math.min(start.row, end.row); r <= rlen; r++) {
  659. row = [];
  660. const physicalRow = this.instance.runHooks('modifyRow', r);
  661. for (c = Math.min(start.col, end.col); c <= clen; c++) {
  662. if (physicalRow === null) {
  663. break;
  664. }
  665. row.push(getFn.call(this, r, this.colToProp(c)));
  666. }
  667. if (physicalRow !== null) {
  668. output.push(row);
  669. }
  670. }
  671. return output;
  672. };
  673. /**
  674. * Return data as text (tab separated columns).
  675. *
  676. * @param {Object} [start] Start selection position. Visual indexes.
  677. * @param {Object} [end] End selection position. Visual indexes.
  678. * @returns {String}
  679. */
  680. DataMap.prototype.getText = function(start, end) {
  681. return SheetClip.stringify(this.getRange(start, end, this.DESTINATION_RENDERER));
  682. };
  683. /**
  684. * Return data as copyable text (tab separated columns intended for clipboard copy to an external application).
  685. *
  686. * @param {Object} [start] Start selection position. Visual indexes.
  687. * @param {Object} [end] End selection position. Visual indexes.
  688. * @returns {String}
  689. */
  690. DataMap.prototype.getCopyableText = function(start, end) {
  691. return SheetClip.stringify(this.getRange(start, end, this.DESTINATION_CLIPBOARD_GENERATOR));
  692. };
  693. /**
  694. * `skipLengthCache` callback.
  695. * @private
  696. * @param {Number} delay Time of the delay in milliseconds.
  697. */
  698. DataMap.prototype.onSkipLengthCache = function(delay) {
  699. this.skipCache = true;
  700. setTimeout(() => {
  701. this.skipCache = false;
  702. }, delay);
  703. };
  704. /**
  705. * Destroy instance.
  706. */
  707. DataMap.prototype.destroy = function() {
  708. this.interval.stop();
  709. this.interval = null;
  710. this.instance = null;
  711. this.priv = null;
  712. this.GridSettings = null;
  713. this.dataSource = null;
  714. this.cachedLength = null;
  715. this.duckSchema = null;
  716. };
  717. export default DataMap;