dynamic-rows.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. /**
  6. * @api
  7. */
  8. define([
  9. 'ko',
  10. 'mageUtils',
  11. 'underscore',
  12. 'uiLayout',
  13. 'uiCollection',
  14. 'uiRegistry',
  15. 'mage/translate'
  16. ], function (ko, utils, _, layout, uiCollection, registry, $t) {
  17. 'use strict';
  18. /**
  19. * Checks value type and cast to boolean if needed
  20. *
  21. * @param {*} value
  22. *
  23. * @returns {Boolean|*} casted or origin value
  24. */
  25. function castValue(value) {
  26. if (_.isUndefined(value) || value === '' || _.isNull(value)) {
  27. return false;
  28. }
  29. return value;
  30. }
  31. /**
  32. * Compares arrays.
  33. *
  34. * @param {Array} base - array as method bases its decision on first argument.
  35. * @param {Array} current - second array
  36. *
  37. * @returns {Boolean} result - is current array equal to base array
  38. */
  39. function compareArrays(base, current) {
  40. var index = 0,
  41. length = base.length;
  42. if (base.length !== current.length) {
  43. return false;
  44. }
  45. /*eslint-disable max-depth, eqeqeq, no-use-before-define */
  46. for (index; index < length; index++) {
  47. if (_.isArray(base[index]) && _.isArray(current[index])) {
  48. if (!compareArrays(base[index], current[index])) {
  49. return false;
  50. }
  51. } else if (typeof base[index] === 'object' && typeof current[index] === 'object') {
  52. if (!compareObjects(base[index], current[index])) {
  53. return false;
  54. }
  55. } else if (castValue(base[index]) != castValue(current[index])) {
  56. return false;
  57. }
  58. }/*eslint-enable max-depth, eqeqeq, no-use-before-define */
  59. return true;
  60. }
  61. /**
  62. * Compares objects. Compares only properties from origin object,
  63. * if current object has more properties - they are not considered
  64. *
  65. * @param {Object} base - first object
  66. * @param {Object} current - second object
  67. *
  68. * @returns {Boolean} result - is current object equal to base object
  69. */
  70. function compareObjects(base, current) {
  71. var prop;
  72. /*eslint-disable max-depth, eqeqeq*/
  73. for (prop in base) {
  74. if (_.isArray(base[prop]) && _.isArray(current[prop])) {
  75. if (!compareArrays(base[prop], current[prop])) {
  76. return false;
  77. }
  78. } else if (typeof base[prop] === 'object' && typeof current[prop] === 'object') {
  79. if (!compareObjects(base[prop], current[prop])) {
  80. return false;
  81. }
  82. } else if (castValue(base[prop]) != castValue(current[prop])) {
  83. return false;
  84. }
  85. }/*eslint-enable max-depth, eqeqeq */
  86. return true;
  87. }
  88. return uiCollection.extend({
  89. defaults: {
  90. defaultRecord: false,
  91. columnsHeader: true,
  92. columnsHeaderAfterRender: false,
  93. columnsHeaderClasses: '',
  94. labels: [],
  95. recordTemplate: 'record',
  96. collapsibleHeader: false,
  97. additionalClasses: {},
  98. visible: true,
  99. disabled: false,
  100. fit: false,
  101. addButton: true,
  102. addButtonLabel: $t('Add'),
  103. recordData: [],
  104. maxPosition: 0,
  105. deleteProperty: 'delete',
  106. identificationProperty: 'record_id',
  107. deleteValue: true,
  108. showSpinner: true,
  109. isDifferedFromDefault: false,
  110. defaultState: [],
  111. defaultPagesState: {},
  112. pagesChanged: {},
  113. hasInitialPagesState: {},
  114. changed: false,
  115. fallbackResetTpl: 'ui/form/element/helper/fallback-reset-link',
  116. dndConfig: {
  117. name: '${ $.name }_dnd',
  118. component: 'Magento_Ui/js/dynamic-rows/dnd',
  119. template: 'ui/dynamic-rows/cells/dnd',
  120. recordsProvider: '${ $.name }',
  121. enabled: true
  122. },
  123. templates: {
  124. record: {
  125. parent: '${ $.$data.collection.name }',
  126. name: '${ $.$data.index }',
  127. dataScope: '${ $.$data.collection.index }.${ $.name }',
  128. nodeTemplate: '${ $.parent }.${ $.$data.collection.recordTemplate }'
  129. }
  130. },
  131. links: {
  132. recordData: '${ $.provider }:${ $.dataScope }.${ $.index }'
  133. },
  134. listens: {
  135. visible: 'setVisible',
  136. disabled: 'setDisabled',
  137. childTemplate: 'initHeader',
  138. recordTemplate: 'onUpdateRecordTemplate',
  139. recordData: 'setDifferedFromDefault parsePagesData setRecordDataToCache',
  140. currentPage: 'changePage',
  141. elems: 'checkSpinner',
  142. changed: 'updateTrigger'
  143. },
  144. modules: {
  145. dnd: '${ $.dndConfig.name }'
  146. },
  147. pages: 1,
  148. pageSize: 20,
  149. relatedData: [],
  150. currentPage: 1,
  151. recordDataCache: [],
  152. startIndex: 0
  153. },
  154. /**
  155. * Sets record data to cache
  156. */
  157. setRecordDataToCache: function (data) {
  158. this.recordDataCache = data;
  159. },
  160. /**
  161. * Extends instance with default config, calls initialize of parent
  162. * class, calls initChildren method, set observe variable.
  163. * Use parent "track" method - wrapper observe array
  164. *
  165. * @returns {Object} Chainable.
  166. */
  167. initialize: function () {
  168. _.bindAll(this,
  169. 'processingDeleteRecord',
  170. 'onChildrenUpdate',
  171. 'checkDefaultState',
  172. 'renderColumnsHeader',
  173. 'deleteHandler',
  174. 'setDefaultState'
  175. );
  176. this._super()
  177. .initChildren()
  178. .initDnd()
  179. .initDefaultRecord()
  180. .setInitialProperty()
  181. .setColumnsHeaderListener()
  182. .checkSpinner();
  183. this.on('recordData', this.checkDefaultState);
  184. return this;
  185. },
  186. /**
  187. * @inheritdoc
  188. */
  189. bubble: function (event) {
  190. if (event === 'deleteRecord' || event === 'update') {
  191. return false;
  192. }
  193. return this._super();
  194. },
  195. /**
  196. * Inits DND module
  197. *
  198. * @returns {Object} Chainable.
  199. */
  200. initDnd: function () {
  201. if (this.dndConfig.enabled) {
  202. layout([this.dndConfig]);
  203. }
  204. return this;
  205. },
  206. /** @inheritdoc */
  207. destroy: function () {
  208. if (this.dnd()) {
  209. this.dnd().destroy();
  210. }
  211. this._super();
  212. },
  213. /**
  214. * Calls 'initObservable' of parent
  215. *
  216. * @returns {Object} Chainable.
  217. */
  218. initObservable: function () {
  219. this._super()
  220. .track('childTemplate')
  221. .observe([
  222. 'pages',
  223. 'currentPage',
  224. 'recordData',
  225. 'columnsHeader',
  226. 'visible',
  227. 'disabled',
  228. 'labels',
  229. 'showSpinner',
  230. 'isDifferedFromDefault',
  231. 'changed'
  232. ]);
  233. return this;
  234. },
  235. /**
  236. * @inheritdoc
  237. */
  238. initElement: function (elem) {
  239. this._super();
  240. elem.on({
  241. 'deleteRecord': this.deleteHandler,
  242. 'update': this.onChildrenUpdate,
  243. 'addChild': this.setDefaultState
  244. });
  245. return this;
  246. },
  247. /**
  248. * Handler for deleteRecord event
  249. *
  250. * @param {Number|String} index - element index
  251. * @param {Number|String} id
  252. */
  253. deleteHandler: function (index, id) {
  254. var defaultState;
  255. this.setDefaultState();
  256. defaultState = this.defaultPagesState[this.currentPage()];
  257. this.processingDeleteRecord(index, id);
  258. this.pagesChanged[this.currentPage()] =
  259. !compareArrays(defaultState, this.arrayFilter(this.getChildItems()));
  260. this.changed(_.some(this.pagesChanged));
  261. },
  262. /**
  263. * Set initial property to records data
  264. *
  265. * @returns {Object} Chainable.
  266. */
  267. setInitialProperty: function () {
  268. if (_.isArray(this.recordData())) {
  269. this.recordData.each(function (data, index) {
  270. this.source.set(this.dataScope + '.' + this.index + '.' + index + '.initialize', true);
  271. }, this);
  272. }
  273. return this;
  274. },
  275. /**
  276. * Handler for update event
  277. *
  278. * @param {Boolean} state
  279. */
  280. onChildrenUpdate: function (state) {
  281. var changed,
  282. dataScope,
  283. changedElemDataScope;
  284. if (state && !this.hasInitialPagesState[this.currentPage()]) {
  285. this.setDefaultState();
  286. changed = this.getChangedElems(this.elems());
  287. dataScope = this.elems()[0].dataScope.split('.');
  288. dataScope.splice(dataScope.length - 1, 1);
  289. changed.forEach(function (elem) {
  290. changedElemDataScope = elem.dataScope.split('.');
  291. changedElemDataScope.splice(0, dataScope.length);
  292. changedElemDataScope[0] =
  293. (parseInt(changedElemDataScope[0], 10) - this.pageSize * (this.currentPage() - 1)).toString();
  294. this.setValueByPath(
  295. this.defaultPagesState[this.currentPage()],
  296. changedElemDataScope, elem.initialValue
  297. );
  298. }, this);
  299. }
  300. if (this.defaultPagesState[this.currentPage()]) {
  301. this.setChangedForCurrentPage();
  302. }
  303. },
  304. /**
  305. * Set default dynamic-rows state or state before changing data
  306. *
  307. * @param {Array} data - defaultState data
  308. */
  309. setDefaultState: function (data) {
  310. var componentData,
  311. childItems;
  312. if (!this.hasInitialPagesState[this.currentPage()]) {
  313. childItems = this.getChildItems();
  314. componentData = childItems.length ?
  315. utils.copy(childItems) :
  316. utils.copy(this.getChildItems(this.recordDataCache));
  317. componentData.forEach(function (dataObj) {
  318. if (dataObj.hasOwnProperty('initialize')) {
  319. delete dataObj.initialize;
  320. }
  321. });
  322. this.hasInitialPagesState[this.currentPage()] = true;
  323. this.defaultPagesState[this.currentPage()] = data ? data : this.arrayFilter(componentData);
  324. }
  325. },
  326. /**
  327. * Sets value to object by string path
  328. *
  329. * @param {Object} obj
  330. * @param {Array|String} path
  331. * @param {*} value
  332. */
  333. setValueByPath: function (obj, path, value) {
  334. var prop;
  335. if (_.isString(path)) {
  336. path = path.split('.');
  337. }
  338. if (path.length - 1) {
  339. prop = obj[path[0]];
  340. path.splice(0, 1);
  341. this.setValueByPath(prop, path, value);
  342. } else if (path.length && obj) {
  343. obj[path[0]] = value;
  344. }
  345. },
  346. /**
  347. * Returns elements which changed self state
  348. *
  349. * @param {Array} array - data array
  350. * @param {Array} changed - array with changed elements
  351. * @returns {Array} changed - array with changed elements
  352. */
  353. getChangedElems: function (array, changed) {
  354. changed = changed || [];
  355. array.forEach(function (elem) {
  356. if (_.isFunction(elem.elems)) {
  357. this.getChangedElems(elem.elems(), changed);
  358. } else if (_.isFunction(elem.hasChanged) && elem.hasChanged()) {
  359. changed.push(elem);
  360. }
  361. }, this);
  362. return changed;
  363. },
  364. /**
  365. * Checks columnsHeaderAfterRender property,
  366. * and set listener on elems if needed
  367. *
  368. * @returns {Object} Chainable.
  369. */
  370. setColumnsHeaderListener: function () {
  371. if (this.columnsHeaderAfterRender) {
  372. this.on('recordData', this.renderColumnsHeader);
  373. if (_.isArray(this.recordData()) && this.recordData().length) {
  374. this.renderColumnsHeader();
  375. }
  376. }
  377. return this;
  378. },
  379. /**
  380. * Checks whether component's state is default or not
  381. */
  382. checkDefaultState: function () {
  383. var isRecordDataArray = _.isArray(this.recordData()),
  384. initialize,
  385. hasNotDefaultRecords = isRecordDataArray ? !!this.recordData().filter(function (data) {
  386. return !data.initialize;
  387. }).length : false;
  388. if (!this.hasInitialPagesState[this.currentPage()] && isRecordDataArray && hasNotDefaultRecords) {
  389. this.hasInitialPagesState[this.currentPage()] = true;
  390. this.defaultPagesState[this.currentPage()] = utils.copy(this.getChildItems().filter(function (data) {
  391. initialize = data.initialize;
  392. delete data.initialize;
  393. return initialize;
  394. }));
  395. this.setChangedForCurrentPage();
  396. } else if (this.hasInitialPagesState[this.currentPage()]) {
  397. this.setChangedForCurrentPage();
  398. }
  399. },
  400. /**
  401. * Filters out deleted items from array
  402. *
  403. * @param {Array} data
  404. *
  405. * @returns {Array} filtered array
  406. */
  407. arrayFilter: function (data) {
  408. var prop;
  409. /*eslint-disable no-loop-func*/
  410. data.forEach(function (elem) {
  411. for (prop in elem) {
  412. if (_.isArray(elem[prop])) {
  413. elem[prop] = _.filter(elem[prop], function (elemProp) {
  414. return elemProp[this.deleteProperty] !== this.deleteValue;
  415. }, this);
  416. elem[prop].forEach(function (elemProp) {
  417. if (_.isArray(elemProp)) {
  418. elem[prop] = this.arrayFilter(elemProp);
  419. }
  420. }, this);
  421. }
  422. }
  423. }, this);
  424. /*eslint-enable no-loop-func*/
  425. return data;
  426. },
  427. /**
  428. * Triggers update event
  429. *
  430. * @param {Boolean} val
  431. */
  432. updateTrigger: function (val) {
  433. this.trigger('update', val);
  434. },
  435. /**
  436. * Returns component state
  437. */
  438. hasChanged: function () {
  439. return this.changed();
  440. },
  441. /**
  442. * Render column header
  443. */
  444. renderColumnsHeader: function () {
  445. this.recordData().length ? this.columnsHeader(true) : this.columnsHeader(false);
  446. },
  447. /**
  448. * Init default record
  449. *
  450. * @returns Chainable.
  451. */
  452. initDefaultRecord: function () {
  453. if (this.defaultRecord && !this.recordData().length) {
  454. this.addChild();
  455. }
  456. return this;
  457. },
  458. /**
  459. * Create header template
  460. *
  461. * @param {Object} prop - instance obj
  462. *
  463. * @returns {Object} Chainable.
  464. */
  465. createHeaderTemplate: function (prop) {
  466. var visible = prop.visible !== false,
  467. disabled = _.isUndefined(prop.disabled) ? this.disabled() : prop.disabled;
  468. return {
  469. visible: ko.observable(visible),
  470. disabled: ko.observable(disabled)
  471. };
  472. },
  473. /**
  474. * Init header elements
  475. */
  476. initHeader: function () {
  477. var labels = [],
  478. data;
  479. if (!this.labels().length) {
  480. _.each(this.childTemplate.children, function (cell) {
  481. data = this.createHeaderTemplate(cell.config);
  482. cell.config.labelVisible = false;
  483. _.extend(data, {
  484. label: cell.config.label,
  485. name: cell.name,
  486. required: !!cell.config.validation,
  487. columnsHeaderClasses: cell.config.columnsHeaderClasses,
  488. sortOrder: cell.config.sortOrder
  489. });
  490. labels.push(data);
  491. }, this);
  492. this.labels(_.sortBy(labels, 'sortOrder'));
  493. }
  494. },
  495. /**
  496. * Set max element position
  497. *
  498. * @param {Number} position - element position
  499. * @param {Object} elem - instance
  500. */
  501. setMaxPosition: function (position, elem) {
  502. if (position || position === 0) {
  503. this.checkMaxPosition(position);
  504. this.sort(position, elem);
  505. } else {
  506. this.maxPosition += 1;
  507. }
  508. },
  509. /**
  510. * Sort element by position
  511. *
  512. * @param {Number} position - element position
  513. * @param {Object} elem - instance
  514. */
  515. sort: function (position, elem) {
  516. var that = this,
  517. sorted,
  518. updatedCollection;
  519. if (this.elems().filter(function (el) {
  520. return el.position || el.position === 0;
  521. }).length !== this.getChildItems().length) {
  522. return false;
  523. }
  524. if (!elem.containers.length) {
  525. registry.get(elem.name, function () {
  526. that.sort(position, elem);
  527. });
  528. return false;
  529. }
  530. sorted = this.elems().sort(function (propOne, propTwo) {
  531. return ~~propOne.position - ~~propTwo.position;
  532. });
  533. updatedCollection = this.updatePosition(sorted, position, elem.name);
  534. this.elems(updatedCollection);
  535. },
  536. /**
  537. * Checking loader visibility
  538. *
  539. * @param {Array} elems
  540. */
  541. checkSpinner: function (elems) {
  542. this.showSpinner(!(!this.recordData().length || elems && elems.length === this.getChildItems().length));
  543. },
  544. /**
  545. * Filtering data and calculates the quantity of pages
  546. *
  547. * @param {Array} data
  548. */
  549. parsePagesData: function (data) {
  550. var pages;
  551. this.relatedData = this.deleteProperty ?
  552. _.filter(data, function (elem) {
  553. return elem && elem[this.deleteProperty] !== this.deleteValue;
  554. }, this) : data;
  555. pages = Math.ceil(this.relatedData.length / this.pageSize) || 1;
  556. this.pages(pages);
  557. },
  558. /**
  559. * Reinit record data in order to remove deleted values
  560. *
  561. * @return void
  562. */
  563. reinitRecordData: function () {
  564. this.recordData(
  565. _.filter(this.recordData(), function (elem) {
  566. return elem && elem[this.deleteProperty] !== this.deleteValue;
  567. }, this)
  568. );
  569. },
  570. /**
  571. * Get items to rendering on current page
  572. *
  573. * @returns {Array} data
  574. */
  575. getChildItems: function (data, page) {
  576. var dataRecord = data || this.relatedData,
  577. startIndex;
  578. this.startIndex = (~~this.currentPage() - 1) * this.pageSize;
  579. startIndex = page || this.startIndex;
  580. return dataRecord.slice(startIndex, this.startIndex + this.pageSize);
  581. },
  582. /**
  583. * Get record count with filtered delete property.
  584. *
  585. * @returns {Number} count
  586. */
  587. getRecordCount: function () {
  588. return _.filter(this.recordData(), function (record) {
  589. return record && record[this.deleteProperty] !== this.deleteValue;
  590. }, this).length;
  591. },
  592. /**
  593. * Get number of columns
  594. *
  595. * @returns {Number} columns
  596. */
  597. getColumnsCount: function () {
  598. return this.labels().length + (this.dndConfig.enabled ? 1 : 0);
  599. },
  600. /**
  601. * Processing pages before addChild
  602. *
  603. * @param {Object} ctx - element context
  604. * @param {Number|String} index - element index
  605. * @param {Number|String} prop - additional property to element
  606. */
  607. processingAddChild: function (ctx, index, prop) {
  608. this.bubble('addChild', false);
  609. if (this.relatedData.length && this.relatedData.length % this.pageSize === 0) {
  610. this.pages(this.pages() + 1);
  611. this.nextPage();
  612. } else if (~~this.currentPage() !== this.pages()) {
  613. this.currentPage(this.pages());
  614. }
  615. this.addChild(ctx, index, prop);
  616. },
  617. /**
  618. * Processing pages before deleteRecord
  619. *
  620. * @param {Number|String} index - element index
  621. * @param {Number|String} recordId
  622. */
  623. processingDeleteRecord: function (index, recordId) {
  624. this.deleteRecord(index, recordId);
  625. },
  626. /**
  627. * Change page
  628. *
  629. * @param {Number} page - current page
  630. */
  631. changePage: function (page) {
  632. this.clear();
  633. if (page === 1 && !this.recordData().length) {
  634. return false;
  635. }
  636. if (~~page > this.pages()) {
  637. this.currentPage(this.pages());
  638. return false;
  639. } else if (~~page < 1) {
  640. this.currentPage(1);
  641. return false;
  642. }
  643. this.initChildren();
  644. return true;
  645. },
  646. /**
  647. * Check page
  648. *
  649. * @returns {Boolean} is page first or not
  650. */
  651. isFirst: function () {
  652. return this.currentPage() === 1;
  653. },
  654. /**
  655. * Check page
  656. *
  657. * @returns {Boolean} is page last or not
  658. */
  659. isLast: function () {
  660. return this.currentPage() === this.pages();
  661. },
  662. /**
  663. * Change page to next
  664. */
  665. nextPage: function () {
  666. this.currentPage(this.currentPage() + 1);
  667. },
  668. /**
  669. * Change page to previous
  670. */
  671. previousPage: function () {
  672. this.currentPage(this.currentPage() - 1);
  673. },
  674. /**
  675. * Check dependency and set position to elements
  676. *
  677. * @param {Array} collection - elems
  678. * @param {Number} position - current position
  679. * @param {String} elemName - element name
  680. *
  681. * @returns {Array} collection
  682. */
  683. updatePosition: function (collection, position, elemName) {
  684. var curPos,
  685. parsePosition = ~~position,
  686. result = _.filter(collection, function (record) {
  687. return ~~record.position === parsePosition;
  688. });
  689. if (result[1]) {
  690. curPos = parsePosition + 1;
  691. result[0].name === elemName ? result[1].position = curPos : result[0].position = curPos;
  692. this.updatePosition(collection, curPos);
  693. }
  694. return collection;
  695. },
  696. /**
  697. * Check max elements position and set if max
  698. *
  699. * @param {Number} position - current position
  700. */
  701. checkMaxPosition: function (position) {
  702. var max = 0,
  703. pos;
  704. this.elems.each(function (record) {
  705. pos = ~~record.position;
  706. pos > max ? max = pos : false;
  707. });
  708. max < position ? max = position : false;
  709. this.maxPosition = max;
  710. },
  711. /**
  712. * Remove and set new max position
  713. */
  714. removeMaxPosition: function () {
  715. this.maxPosition = 0;
  716. this.elems.each(function (record) {
  717. this.maxPosition < record.position ? this.maxPosition = ~~record.position : false;
  718. }, this);
  719. },
  720. /**
  721. * Update record template and rerender elems
  722. *
  723. * @param {String} recordName - record name
  724. */
  725. onUpdateRecordTemplate: function (recordName) {
  726. if (recordName) {
  727. this.recordTemplate = recordName;
  728. this.reload();
  729. }
  730. },
  731. /**
  732. * Delete record
  733. *
  734. * @param {Number} index - row index
  735. *
  736. */
  737. deleteRecord: function (index, recordId) {
  738. var recordInstance,
  739. lastRecord,
  740. recordsData,
  741. lastRecordIndex;
  742. if (this.deleteProperty) {
  743. recordsData = this.recordData();
  744. recordInstance = _.find(this.elems(), function (elem) {
  745. return elem.index === index;
  746. });
  747. recordInstance.destroy();
  748. this.elems([]);
  749. this._updateCollection();
  750. this.removeMaxPosition();
  751. recordsData[recordInstance.index][this.deleteProperty] = this.deleteValue;
  752. this.recordData(recordsData);
  753. this.reinitRecordData();
  754. this.reload();
  755. } else {
  756. this.update = true;
  757. if (~~this.currentPage() === this.pages()) {
  758. lastRecordIndex = this.startIndex + this.getChildItems().length - 1;
  759. lastRecord =
  760. _.findWhere(this.elems(), {
  761. index: lastRecordIndex
  762. }) ||
  763. _.findWhere(this.elems(), {
  764. index: lastRecordIndex.toString()
  765. });
  766. lastRecord.destroy();
  767. }
  768. this.removeMaxPosition();
  769. recordsData = this._getDataByProp(recordId);
  770. this._updateData(recordsData);
  771. this.update = false;
  772. }
  773. this._reducePages();
  774. this._sort();
  775. },
  776. /**
  777. * Reduce the number of pages
  778. *
  779. * @private
  780. * @return void
  781. */
  782. _reducePages: function () {
  783. if (this.pages() < ~~this.currentPage()) {
  784. this.currentPage(this.pages());
  785. }
  786. },
  787. /**
  788. * Get data object by some property
  789. *
  790. * @param {Number} id - element id
  791. * @param {String} prop - property
  792. */
  793. _getDataByProp: function (id, prop) {
  794. prop = prop || this.identificationProperty;
  795. return _.reject(this.getChildItems(), function (recordData) {
  796. return recordData[prop].toString() === id.toString();
  797. }, this);
  798. },
  799. /**
  800. * Sort elems by position property
  801. */
  802. _sort: function () {
  803. this.elems(this.elems().sort(function (propOne, propTwo) {
  804. return ~~propOne.position - ~~propTwo.position;
  805. }));
  806. },
  807. /**
  808. * Set new data to dataSource,
  809. * delete element
  810. *
  811. * @param {Array} data - record data
  812. */
  813. _updateData: function (data) {
  814. var elems = _.clone(this.elems()),
  815. path,
  816. dataArr;
  817. dataArr = this.recordData.splice(this.startIndex, this.recordData().length - this.startIndex);
  818. dataArr.splice(0, this.pageSize);
  819. elems = _.sortBy(this.elems(), function (elem) {
  820. return ~~elem.index;
  821. });
  822. data.concat(dataArr).forEach(function (rec, idx) {
  823. if (elems[idx]) {
  824. elems[idx].recordId = rec[this.identificationProperty];
  825. }
  826. if (!rec.position) {
  827. rec.position = this.maxPosition;
  828. this.setMaxPosition();
  829. }
  830. path = this.dataScope + '.' + this.index + '.' + (this.startIndex + idx);
  831. this.source.set(path, rec);
  832. }, this);
  833. this.elems(elems);
  834. },
  835. /**
  836. * Rerender dynamic-rows elems
  837. */
  838. reload: function () {
  839. this.clear();
  840. this.initChildren(false, true);
  841. },
  842. /**
  843. * Destroy all dynamic-rows elems
  844. *
  845. * @returns {Object} Chainable.
  846. */
  847. clear: function () {
  848. this.destroyChildren();
  849. return this;
  850. },
  851. /**
  852. * Reset data to initial value.
  853. * Call method reset on child elements.
  854. */
  855. reset: function () {
  856. var elems = this.elems();
  857. _.each(elems, function (elem) {
  858. if (_.isFunction(elem.reset)) {
  859. elem.reset();
  860. }
  861. });
  862. },
  863. /**
  864. * Set classes
  865. *
  866. * @param {Object} data
  867. *
  868. * @returns {Object} Classes
  869. */
  870. setClasses: function (data) {
  871. var additional;
  872. if (_.isString(data.additionalClasses)) {
  873. additional = data.additionalClasses.split(' ');
  874. data.additionalClasses = {};
  875. additional.forEach(function (name) {
  876. data.additionalClasses[name] = true;
  877. });
  878. }
  879. if (!data.additionalClasses) {
  880. data.additionalClasses = {};
  881. }
  882. _.extend(data.additionalClasses, {
  883. '_fit': data.fit,
  884. '_required': data.required,
  885. '_error': data.error,
  886. '_empty': !this.elems().length,
  887. '_no-header': this.columnsHeaderAfterRender || this.collapsibleHeader
  888. });
  889. return data.additionalClasses;
  890. },
  891. /**
  892. * Initialize children
  893. *
  894. * @returns {Object} Chainable.
  895. */
  896. initChildren: function () {
  897. this.showSpinner(true);
  898. this.getChildItems().forEach(function (data, index) {
  899. this.addChild(data, this.startIndex + index);
  900. }, this);
  901. return this;
  902. },
  903. /**
  904. * Set visibility to dynamic-rows child
  905. *
  906. * @param {Boolean} state
  907. */
  908. setVisible: function (state) {
  909. this.elems.each(function (record) {
  910. record.setVisible(state);
  911. }, this);
  912. },
  913. /**
  914. * Set disabled property to dynamic-rows child
  915. *
  916. * @param {Boolean} state
  917. */
  918. setDisabled: function (state) {
  919. this.elems.each(function (record) {
  920. record.setDisabled(state);
  921. }, this);
  922. },
  923. /**
  924. * Set visibility to column
  925. *
  926. * @param {Number} index - column index
  927. * @param {Boolean} state
  928. */
  929. setVisibilityColumn: function (index, state) {
  930. this.elems.each(function (record) {
  931. record.setVisibilityColumn(index, state);
  932. }, this);
  933. },
  934. /**
  935. * Set disabled property to column
  936. *
  937. * @param {Number} index - column index
  938. * @param {Boolean} state
  939. */
  940. setDisabledColumn: function (index, state) {
  941. this.elems.each(function (record) {
  942. record.setDisabledColumn(index, state);
  943. }, this);
  944. },
  945. /**
  946. * Add child components
  947. *
  948. * @param {Object} data - component data
  949. * @param {Number} index - record(row) index
  950. * @param {Number|String} prop - custom identify property
  951. *
  952. * @returns {Object} Chainable.
  953. */
  954. addChild: function (data, index, prop) {
  955. var template = this.templates.record,
  956. child;
  957. index = index || _.isNumber(index) ? index : this.recordData().length;
  958. prop = prop || _.isNumber(prop) ? prop : index;
  959. _.extend(this.templates.record, {
  960. recordId: prop
  961. });
  962. child = utils.template(template, {
  963. collection: this,
  964. index: index
  965. });
  966. layout([child]);
  967. return this;
  968. },
  969. /**
  970. * Restore value to default
  971. */
  972. restoreToDefault: function () {
  973. this.recordData(utils.copy(this.default));
  974. this.reload();
  975. },
  976. /**
  977. * Update whether value differs from default value
  978. */
  979. setDifferedFromDefault: function () {
  980. var recordData = utils.copy(this.recordData());
  981. Array.isArray(recordData) && recordData.forEach(function (item) {
  982. delete item['record_id'];
  983. });
  984. this.isDifferedFromDefault(!_.isEqual(recordData, this.default));
  985. },
  986. /**
  987. * Set the changed property if the current page is different
  988. * than the default state
  989. *
  990. * @return void
  991. */
  992. setChangedForCurrentPage: function () {
  993. this.pagesChanged[this.currentPage()] =
  994. !compareArrays(this.defaultPagesState[this.currentPage()], this.arrayFilter(this.getChildItems()));
  995. this.changed(_.some(this.pagesChanged));
  996. }
  997. });
  998. });