diff --git a/addon/-private/collapse-tree.js b/addon/-private/collapse-tree.js index d982432df..f8b86c888 100644 --- a/addon/-private/collapse-tree.js +++ b/addon/-private/collapse-tree.js @@ -27,6 +27,7 @@ export class TableRowMeta extends EmberObject { */ _cellMetaCache = new Map(); _isCollapsed = false; + _lastKnownIndex = null; @computed('_rowValue.isCollapsed') get isCollapsed() { @@ -95,6 +96,18 @@ export class TableRowMeta extends EmberObject { return parentMeta ? get(parentMeta, 'depth') + 1 : 0; } + @computed('_lastKnownIndex', '_prevSiblingMeta.index') + get index() { + let prevSiblingIndex = get(this, '_prevSiblingMeta.index'); + let lastKnownIndex = get(this, '_lastKnownIndex'); + + if (lastKnownIndex === prevSiblingIndex) { + return lastKnownIndex + 1; + } + + return lastKnownIndex; + } + @computed('_tree.length') get first() { if (get(this, '_tree.length') === 0) { @@ -112,18 +125,18 @@ export class TableRowMeta extends EmberObject { @computed('_tree.length') get next() { let tree = get(this, '_tree'); - if (get(this, 'index') + 1 >= get(tree, 'length')) { + if (get(this, '_lastKnownIndex') + 1 >= get(tree, 'length')) { return null; } - return tree.objectAt(get(this, 'index') + 1); + return tree.objectAt(get(this, '_lastKnownIndex') + 1); } @computed('_tree.length') get prev() { - if (get(this, 'index') === 0) { + if (get(this, '_lastKnownIndex') === 0) { return null; } - return get(this, '_tree').objectAt(get(this, 'index') - 1); + return get(this, '_tree').objectAt(get(this, '_lastKnownIndex') - 1); } toggleCollapse() { @@ -692,18 +705,28 @@ export default class CollapseTree extends EmberObject.extend(EmberArray) { @return {{ value: object, parents: Array<object> }} */ objectAt(index) { - if (index >= get(this, 'length') || index < 0) { + let length = get(this, 'length'); + if (index >= length || index < 0) { return undefined; } + let root = get(this, 'root'); + let rowMetaCache = this.get('rowMetaCache'); + // We add a "fake" top level node to account for the root node let normalizedIndex = index + 1; - let result = get(this, 'root').objectAt(normalizedIndex); - let meta = this.get('rowMetaCache').get(result); + let result = root.objectAt(normalizedIndex); + let meta = rowMetaCache.get(result); - // Set the perceived index on the meta. It should be safe to do this here, since - // the row will always be retrieved via `objectAt` before being used. - set(meta, 'index', index); + // Set the last known index on the meta and link the next siblings meta + // so that its index can recompute in case it conflicts from shifting + set(meta, '_lastKnownIndex', index); + + if (index < length - 1) { + let nextSibling = root.objectAt(normalizedIndex + 1); + let nextMeta = rowMetaCache.get(nextSibling); + set(nextMeta, '_prevSiblingMeta', meta); + } return result; } diff --git a/tests/unit/-private/collapse-tree-test.js b/tests/unit/-private/collapse-tree-test.js index 98ea7c493..e7c2d18a7 100644 --- a/tests/unit/-private/collapse-tree-test.js +++ b/tests/unit/-private/collapse-tree-test.js @@ -173,6 +173,25 @@ module('Unit | Private | CollapseTree', function(hooks) { } }); + test('rowMeta index is recomputed when row is added or removed', function(assert) { + let rows = generateTree([1, [2, 3, [4, 5], 6], 7]); + tree = CollapseTree.create({ rows, rowMetaCache, enableTree: true }); + + let nodes = tree.toArray(); + nodes.forEach((node, i) => assert.equal(metaFor(node).get('index'), i)); + + rows.unshiftObject({ value: 0 }); + + let firstNode = run(() => tree.objectAt(0)); + nodes = [firstNode].concat(nodes); + nodes.forEach((node, i) => assert.equal(metaFor(node).get('index'), i)); + + rows.pushObject({ value: 8 }); + + let lastNode = run(() => tree.objectAt(8)); + nodes.concat(lastNode).forEach((node, i) => assert.equal(metaFor(node).get('index'), i)); + }); + test('can disable tree', function(assert) { tree = CollapseTree.create({ rows: generateTree([0, [1, 2]]),