Skip to content

Commit

Permalink
[feat] Implement Column Sorting and Actions
Browse files Browse the repository at this point in the history
This PR implements column sorting with a default sort implementation,
and dropdown actions, which can be passed in at the header cell level.
Dropdown actions can be used to add buttons to the dropdown menu
associated with each column.

The sorting functionality is accomplished via merge sorting each level
of the CollapseTree. This also leaves the default sort intact, so users
can return to the default state.

Dropdown actions have been tested and documented, but the docs are not
exposed via navigation yet. Unsure of the API at this point.

Styles to come in the next PR.
  • Loading branch information
pzuraq committed May 23, 2018
1 parent f5fd28b commit a798100
Show file tree
Hide file tree
Showing 27 changed files with 1,188 additions and 121 deletions.
55 changes: 54 additions & 1 deletion addon-test-support/pages/-private/ember-table-header.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import PageObject, { collection, hasClass } from 'ember-classy-page-object';
import PageObject, { alias, collection, hasClass } from 'ember-classy-page-object';
import { findElement } from 'ember-classy-page-object/extend';
import { click } from 'ember-native-dom-helpers';

import AddeDropdownPage from '@addepar/pop-menu/test-support/pages/adde-dropdown';
import AddeSubDropdownPage from '@addepar/pop-menu/test-support/pages/adde-sub-dropdown';

import { mouseDown, mouseMove, mouseUp } from '../../helpers/mouse';
import { getScale } from '../../helpers/element';
Expand Down Expand Up @@ -62,6 +66,55 @@ const Header = PageObject.extend({
await mouseMove(header, startX + deltaX, header.clientHeight / 2);
await mouseUp(header, startX + deltaX, header.clientHeight / 2);
},

sortToggle: {
scope: '[data-test-sort-toggle]',

/**
Helper function to click with options like the meta key and ctrl key set
@param {Object} options - click event options
*/
async clickWith(options) {
await click(findElement(this), options);
},
},

sortIndicator: {
scope: '[data-test-sort-indicator]',
isAscending: hasClass('ascending'),
isDescending: hasClass('descending'),
},

actionDropdown: {
scope: '[data-test-action-dropdown]',

dropdown: AddeDropdownPage.extend({
scope: '[data-test-action-dropdown-menu]',

content: {
items: collection({
scope: 'li > button',

subDropdown: AddeSubDropdownPage.extend({
scope: '[data-test-action-sub-dropdown-menu]',

content: {
items: collection({
scope: 'li > button',
}),
},
}),

subActions: alias('subDropdown.content.items'),
}),
},
}),

open: alias('dropdown.open'),
close: alias('dropdown.close'),
items: alias('dropdown.content.items'),
},
});

export default {
Expand Down
68 changes: 46 additions & 22 deletions addon/-private/collapse-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { addObserver } from '@ember/object/observers';
import { objectAt } from './utils/array';
import { notifyPropertyChange } from './utils/ember';
import { getOrCreate } from './meta-cache';
import { mergeSort } from './utils/sort';

export const SELECT_MODE = {
SINGLE: 'single',
Expand Down Expand Up @@ -69,7 +70,7 @@ class TableRowMeta extends EmberObject {
return selectedRows.includes(rowValue) || get(this, '_parentMeta.isSelected');
}

@computed('_tree.{enableTree,enableCollapse}', 'value.children.[]')
@computed('_tree.{enableTree,enableCollapse}', '_rowValue.children.[]')
get canCollapse() {
if (!get(this, '_tree.enableTree') || !get(this, '_tree.enableCollapse')) {
return false;
Expand Down Expand Up @@ -256,7 +257,7 @@ function closestLessThan(values, target) {
Single node of a CollapseTree
*/
class CollapseTreeNode extends EmberObject {
_children = null;
_childNodes = null;

constructor() {
super(...arguments);
Expand Down Expand Up @@ -288,22 +289,22 @@ class CollapseTreeNode extends EmberObject {
destroy() {
super.destroy(...arguments);

this.cleanChildren();
this.cleanChildNodes();
}

/**
Fully destroys the child nodes in the event that they change or that this
node is destroyed. If children are not destroyed, they will leak memory due
to dangling references in Ember Meta.
*/
cleanChildren() {
if (this._children) {
for (let child of this._children) {
cleanChildNodes() {
if (this._childNodes) {
for (let child of this._childNodes) {
if (child instanceof CollapseTreeNode) {
child.destroy();
}
}
this._children = null;
this._childNodes = null;
}
}

Expand All @@ -325,6 +326,23 @@ class CollapseTreeNode extends EmberObject {
return !get(this, 'value.children').some(child => isArray(get(child, 'children')));
}

@computed('value.children.[]', 'tree.{sorts.[],sortFunction,compareFunction}')
get sortedChildren() {
let valueChildren = get(this, 'value.children');

let sorts = get(this, 'tree.sorts');
let sortFunction = get(this, 'tree.sortFunction');
let compareFunction = get(this, 'tree.compareFunction');

if (sortFunction && compareFunction && sorts && get(sorts, 'length') > 0) {
valueChildren = mergeSort(valueChildren, (itemA, itemB) => {
return sortFunction(itemA, itemB, sorts, compareFunction);
});
}

return valueChildren;
}

/**
The children of this node, if they exist. Children can be other nodes, or
spans (arrays) of leaf value-nodes. For instance:
Expand Down Expand Up @@ -352,25 +370,25 @@ class CollapseTreeNode extends EmberObject {
@type Array<Node|Array<object>>
*/
@computed('value.children.[]', 'isLeaf')
get children() {
this.cleanChildren();
@computed('sortedChildren.[]', 'isLeaf')
get childNodes() {
this.cleanChildNodes();

if (get(this, 'isLeaf')) {
return null;
}

let valueChildren = get(this, 'value.children');
let sortedChildren = get(this, 'sortedChildren');
let tree = get(this, 'tree');
let children = [];
let sliceStart = false;

valueChildren.forEach((child, index) => {
sortedChildren.forEach((child, index) => {
let grandchildren = get(child, 'children');

if (isArray(grandchildren) && get(grandchildren, 'length') > 0) {
if (sliceStart !== false) {
children.push(valueChildren.slice(sliceStart, index));
children.push(sortedChildren.slice(sliceStart, index));
sliceStart = false;
}

Expand All @@ -381,10 +399,10 @@ class CollapseTreeNode extends EmberObject {
});

if (sliceStart !== false) {
children.push(valueChildren.slice(sliceStart));
children.push(sortedChildren.slice(sliceStart));
}

this._children = children;
this._childNodes = children;

return children;
}
Expand All @@ -400,14 +418,20 @@ class CollapseTreeNode extends EmberObject {
length of its value-children.
3. Otherwise, the length is the sum of the lengths of its children.
*/
@computed('rowMeta.isCollapsed', 'value.children.[]', 'tree.enableTree', 'isLeaf')
@computed(
'childNodes.[]',
'sortedChildren.[]',
'isLeaf',
'rowMeta.isCollapsed',
'tree.enableTree'
)
get length() {
if (get(this, 'rowMeta.isCollapsed') === true) {
return 1;
} else if (get(this, 'isLeaf')) {
return 1 + get(this, 'value.children.length');
return 1 + get(this, 'sortedChildren.length');
} else {
return 1 + get(this, 'children').reduce((sum, child) => sum + get(child, 'length'), 0);
return 1 + get(this, 'childNodes').reduce((sum, child) => sum + get(child, 'length'), 0);
}
}

Expand Down Expand Up @@ -452,7 +476,7 @@ class CollapseTreeNode extends EmberObject {
let offset = 0;
let offsetList = [];

for (let child of get(this, 'children')) {
for (let child of get(this, 'childNodes')) {
offsetList.push(offset);
offset += get(child, 'length');
}
Expand Down Expand Up @@ -495,19 +519,19 @@ class CollapseTreeNode extends EmberObject {
let tree = get(this, 'tree');

if (get(this, 'isLeaf')) {
let value = objectAt(get(this, 'value.children'), normalizedIndex);
let value = objectAt(get(this, 'sortedChildren'), normalizedIndex);
setupRowMeta(tree, value, get(this, 'value'));

return value;
}

let children = get(this, 'children');
let childNodes = get(this, 'childNodes');
let offsetList = get(this, 'offsetList');
let offsetIndex = closestLessThan(offsetList, normalizedIndex);

normalizedIndex = normalizedIndex - offsetList[offsetIndex];

let child = children[offsetIndex];
let child = childNodes[offsetIndex];

if (Array.isArray(child)) {
let value = child[normalizedIndex];
Expand Down
3 changes: 2 additions & 1 deletion addon/-private/column-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { readOnly } from '@ember-decorators/object/computed';
import { scheduler, Token } from 'ember-raf-scheduler';

import { getOrCreate } from './meta-cache';
import { move, splice, mergeSort } from './utils/array';
import { move, splice } from './utils/array';
import { mergeSort } from './utils/sort';
import { getScale, getOuterClientRect, getInnerClientRect } from './utils/element';
import { MainIndicator, DropIndicator } from './utils/reorder-indicators';
import { notifyPropertyChange } from './utils/ember';
Expand Down
54 changes: 0 additions & 54 deletions addon/-private/utils/array.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { compare } from '@ember/utils';
import { isArray } from '@ember/array';
import { assert } from '@ember/debug';

Expand Down Expand Up @@ -39,56 +38,3 @@ export function move(items, start, end) {
splice(items, start, 1);
splice(items, end, 0, sourceItem);
}

function merge(left, right, comparator) {
let mergedArray = [];
let leftIndex = 0;
let rightIndex = 0;

while (leftIndex < left.length && rightIndex < right.length) {
let comparison = comparator(left[leftIndex], right[rightIndex]);

if (comparison <= 0) {
mergedArray.push(left[leftIndex]);
leftIndex++;
} else {
mergedArray.push(right[rightIndex]);
rightIndex++;
}
}

if (leftIndex < left.length) {
mergedArray.splice(mergedArray.length, 0, ...left.slice(leftIndex));
}

if (rightIndex < right.length) {
mergedArray.splice(mergedArray.length, 0, ...right.slice(rightIndex));
}

return mergedArray;
}

/**
* An implementation of the standard merge sort algorithm.
*
* This is necessary because we need a stable sorting algorithm that accepts
* a general comparator. The built in sort function and Ember's sort functions
* are not stable, and `_.sortBy` doesn't take a general comparator. Ideally
* lodash would add a `_.sort` function whose API would mimic this function's.
*
* @function
* @param {Array} array The array to be sorted
* @param {Comparator} comparator The comparator function to compare elements with.
* @returns {Array} A sorted array
*/
export function mergeSort(array, comparator = compare) {
if (array.length <= 1) {
return array;
}

let middleIndex = Math.floor(array.length / 2);
let leftArray = mergeSort(array.slice(0, middleIndex), comparator);
let rightArray = mergeSort(array.slice(middleIndex), comparator);

return merge(leftArray, rightArray, comparator);
}
Loading

0 comments on commit a798100

Please sign in to comment.