blob: 8877c498cd2daf716cadefe6eda87891cd9ec64d [file] [log] [blame]
/**
* @module modules/sort-sk
* @description <h2><code>sort-sk</code></h2>
*
* Allows sorting the members of the indicated element by the values of the
* data attributes.
*
* This element does *not* support the elements to sort changing. It assumes the data in
* the table is constant (otherwise, lit-html will not be able to render it accurately).
*
* Add children to <sort-sk> that generate click events and that have child
* content, such as buttons. Add a data-key * attribute to each child element
* that indicates which data-* attribute the children should be sorted on.
*
* Note that all sorting is done numerically, unless the
* 'data-sort-type=alpha' attribute is set on the element generating the
* click, in which case the sorting is done alphabetically.
*
* Additionally a single child element can have a data-default attribute with
* a value of 'up' or 'down' to indicate the default sorting that already
* exists in the data.
*
*
* @example An example usage, that will present two buttons to sort the contents of
* div#stuffToBeSorted.
*
* <sort-sk target=stuffToBeSorted>
* <button data-key=clustersize data-default=down>Cluster Size </button>
* <button data-key=stepsize data-sort-type=alpha>Name</button>
* </sort-sk>
*
* <div id=stuffToBeSorted>
* <div data-clustersize=10 data-name=foo></div>
* <div data-clustersize=50 data-name=bar></div>
* ...
* </div>
*
* @attr target - The id of the container element whose children are to be sorted.
*
*/
import { define } from 'elements-sk/define';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import { $, $$ } from 'common-sk/modules/dom';
import 'elements-sk/icon/arrow-drop-down-icon-sk';
import 'elements-sk/icon/arrow-drop-up-icon-sk';
export type SortDirection = 'down' | 'up';
// The states to move each button through on a click.
const toggle = (value: string): SortDirection => {
return value === 'down' ? 'up' : 'down';
};
interface SortableEntry {
value: string;
valueAsNumber: number;
node: HTMLElement;
}
// Functions to pass to sort().
const f_alpha_up = (x: SortableEntry, y: SortableEntry) => {
return x.value.localeCompare(y.value);
};
const f_alpha_down = (x: SortableEntry, y: SortableEntry) => f_alpha_up(y, x);
const f_num_up = (x: SortableEntry, y: SortableEntry) => {
if (x.valueAsNumber === y.valueAsNumber) {
return 0;
} else if (x.valueAsNumber > y.valueAsNumber) {
return 1;
} else {
return -1;
}
};
const f_num_down = (x: SortableEntry, y: SortableEntry) => f_num_up(y, x);
export class SortSk extends ElementSk {
connectedCallback() {
super.connectedCallback();
$('[data-key]', this).forEach((ele) => {
// Only attach the icons once.
if (ele.querySelector('arrow-drop-down-icon-sk')) {
return;
}
ele.appendChild(document.createElement('arrow-drop-down-icon-sk'));
ele.appendChild(document.createElement('arrow-drop-up-icon-sk'));
ele.addEventListener('click', (e) => this._clickHandler(e));
});
// Handle a default value if one has been set.
const def = $$<HTMLElement>('[data-default]', this);
if (def) {
this._setSortClass(def, def.dataset.default! as SortDirection);
}
}
_setSortClass(ele: Element, value: SortDirection) {
ele.setAttribute('data-sort-sk', value);
}
_clearSortClass(ele: Element) {
ele.removeAttribute('data-sort-sk');
}
_getSortClass(ele: Element) {
return ele.getAttribute('data-sort-sk') || '';
}
_clickHandler(e: Event) {
let ele = e.target! as HTMLElement;
// The click might have been on something inside the button (e.g. on the arrow-drop-up-icon-sk),
// so we want to bubble up to where the key is and set the class that displays the appropriate
// arrow.
while (!ele.hasAttribute('data-key') && ele.parentElement !== this) {
if (ele.parentElement === null) {
break;
}
ele = ele.parentElement;
}
const dir = toggle(this._getSortClass(ele));
$('[data-key]', this).forEach((e) => {
this._clearSortClass(e);
});
this._setSortClass(ele, dir);
// Are we sorting alphabetically or numerically.
const alpha = ele.dataset.sortType === 'alpha';
// Sort the children of the element at #target.
const sortBy = ele.dataset.key || '(key not found)';
this.sort(sortBy, dir, alpha);
}
/** Re-sort the data by the given key in the given direction. If alpha is true, it will
* sort the data as if it were a string (using localeCompare).
*/
sort(key: string, dir: SortDirection, alpha: boolean) {
// Remember the direction we are sorting in.
const up = dir === 'up';
const container = this.parentElement!.querySelector(
`#${this.getAttribute('target')}`
);
if (container === null) {
throw 'Failed to find "target" attribute.';
}
const arr: SortableEntry[] = [];
for (const ele of Array.from(container.children)) {
const htmlEle = ele as HTMLElement;
const value: string = htmlEle.dataset[key] || '';
const entry = {
value: value,
valueAsNumber: +value,
node: htmlEle,
};
arr.push(entry);
}
// Pick the desired sort function.
let f = f_alpha_up;
if (alpha) {
f = up ? f_alpha_up : f_alpha_down;
} else {
f = up ? f_num_up : f_num_down;
}
arr.sort(f);
// Rearrange the elements in the sorted order.
arr.forEach((e) => {
// Reminder: appendChild will *move* existing nodes, which is what we want here while
// reordering things.
container!.appendChild(e.node);
});
}
}
define('sort-sk', SortSk);