Add sort-sk element to infra-sk.

Bug: skia:9219
Change-Id: Iac57e911a621f9860051a24effa7ae0c6eab08ca
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/232717
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
diff --git a/infra-sk/modules/sort-sk/index.js b/infra-sk/modules/sort-sk/index.js
new file mode 100644
index 0000000..6d80b81
--- /dev/null
+++ b/infra-sk/modules/sort-sk/index.js
@@ -0,0 +1,2 @@
+import './sort-sk.js'
+import './sort-sk.scss'
diff --git a/infra-sk/modules/sort-sk/sort-sk-demo.html b/infra-sk/modules/sort-sk/sort-sk-demo.html
new file mode 100644
index 0000000..f4dd1ca
--- /dev/null
+++ b/infra-sk/modules/sort-sk/sort-sk-demo.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>sort-sk</title>
+  <meta charset="utf-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <style type="text/css" media="screen">
+button {
+  margin: 0 1em;
+}
+  </style>
+</head>
+<body>
+  <h1>sort-sk</h1>
+  <h2>Numeric</h2>
+  <sort-sk target=stuffToBeSorted>
+    <button data-key=clustersize data-default=up>Cluster Size</button>
+    <button data-key=stepsize>Step Size</button>
+  </sort-sk>
+
+  <div id=stuffToBeSorted>
+    <pre data-clustersize=10  data-stepsize=1.2>Size=10   Step=1.2</pre>
+    <pre data-clustersize=50  data-stepsize=0.5>Size=50   Step=0.5</pre>
+    <pre data-clustersize=100 data-stepsize=0.6>Size=100  Step=0.6</pre>
+  </div>
+
+  <h2>Alpha</h2>
+  <sort-sk target=stuffToBeSorted2>
+    <button data-key=name data-default=down data-sort-type=alpha>Name</button>
+    <button data-key=level                  data-sort-type=alpha>Level</button>
+  </sort-sk>
+
+  <div id=stuffToBeSorted2>
+    <pre data-name=foo data-level=beta >foo beta</pre>
+    <pre data-name=baz data-level=alpha>baz alpha</pre>
+    <pre data-name=bar data-level=gamma>bar gamma</pre>
+  </div>
+</body>
+</html>
diff --git a/infra-sk/modules/sort-sk/sort-sk-demo.js b/infra-sk/modules/sort-sk/sort-sk-demo.js
new file mode 100644
index 0000000..1c51332
--- /dev/null
+++ b/infra-sk/modules/sort-sk/sort-sk-demo.js
@@ -0,0 +1 @@
+import './index.js'
diff --git a/infra-sk/modules/sort-sk/sort-sk.js b/infra-sk/modules/sort-sk/sort-sk.js
new file mode 100644
index 0000000..e700eea
--- /dev/null
+++ b/infra-sk/modules/sort-sk/sort-sk.js
@@ -0,0 +1,143 @@
+/**
+ * @module module/sort-sk
+ * @description <h2><code>sort-sk</code></h2>
+ *
+ * Allows sorting the members of the indicated element by the values of the
+ * data attributes.
+ *
+ * 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 { html, render } from 'lit-html'
+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'
+
+// The states to move each button through on a click.
+const toggle = {
+  '': 'down',
+  'down': 'up',
+  'up': 'down',
+};
+
+// Functions to pass to sort().
+const f_alpha_up = (x, y) => {
+  if (x.value === y.value) {
+    return 0;
+  }
+  return  x.value > y.value ? 1 : -1;
+};
+const f_alpha_down = (x, y) => f_alpha_up(y, x);
+const f_num_up = (x, y) => (x.value - y.value);
+const f_num_down = (x, y) => f_num_up(y, x);
+
+window.customElements.define('sort-sk', class 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 = $$('[data-default]', this);
+    if (def) {
+      this._setSortClass(def, def.dataset.default);
+    }
+  }
+
+  _setSortClass(ele, value) {
+    ele.setAttribute('data-sort-sk', value);
+  }
+
+  _clearSortClass(ele) {
+    ele.removeAttribute('data-sort-sk');
+  }
+
+  _getSortClass(ele) {
+    return ele.getAttribute('data-sort-sk') || '';
+  }
+
+  _clickHandler(e) {
+    let ele = e.target;
+    while (ele.parentNode !== this) {
+      ele = ele.parentNode;
+    }
+
+    const dir = toggle[this._getSortClass(ele)];
+
+    $('[data-key]', this).forEach((e) => {
+      this._clearSortClass(e);
+    });
+    this._setSortClass(ele, dir);
+
+    // Remember the direction we are sorting in.
+    let up = dir === 'up';
+
+    // Are we sorting alphabetically or numerically.
+    let alpha = ele.dataset.sortType === 'alpha';
+
+    // Sort the children of the element at #target.
+    let sortBy = ele.dataset.key;
+    let container = this.parentElement.querySelector(`#${this.getAttribute('target')}`);
+    let arr = [];
+    for (const ele of container.children) {
+      let value = ele.dataset[sortBy];
+      if (!alpha) {
+        value = +value;
+      }
+      arr.push({
+        value: value,
+        node: ele
+      });
+    }
+
+    // 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) => {
+      container.appendChild(e.node);
+    });
+  }
+});
+
diff --git a/infra-sk/modules/sort-sk/sort-sk.scss b/infra-sk/modules/sort-sk/sort-sk.scss
new file mode 100644
index 0000000..cc95479
--- /dev/null
+++ b/infra-sk/modules/sort-sk/sort-sk.scss
@@ -0,0 +1,34 @@
+@import '~elements-sk/colors';
+
+sort-sk {
+  display: flex;
+
+
+  [data-sort-sk="up"] {
+    arrow-drop-up-icon-sk {
+      visibility: visible;
+      display: inline-block;
+    }
+    arrow-drop-down-icon-sk {
+      display: none;
+    }
+  }
+
+  [data-sort-sk="down"] {
+    arrow-drop-up-icon-sk {
+      visibility: hidden;
+      display: none;
+    }
+    arrow-drop-down-icon-sk {
+      display: inline-block;
+    }
+  }
+
+  arrow-drop-down-icon-sk {
+    display: none;
+  }
+  arrow-drop-up-icon-sk {
+    visibility: hidden;
+    display: inline-block;
+  }
+}
diff --git a/infra-sk/modules/sort-sk/sort-sk_test.js b/infra-sk/modules/sort-sk/sort-sk_test.js
new file mode 100644
index 0000000..e941501
--- /dev/null
+++ b/infra-sk/modules/sort-sk/sort-sk_test.js
@@ -0,0 +1,72 @@
+import './index.js'
+import { $ } from 'common-sk/modules/dom'
+
+let container = document.createElement('div');
+document.body.appendChild(container);
+
+afterEach(function() {
+  container.innerHTML = "";
+});
+
+describe('sort-sk', function() {
+  it('sorts numerically by default', function() {
+    return window.customElements.whenDefined('sort-sk').then(() => {
+      container.innerHTML = `
+  <sort-sk target=stuffToBeSorted>
+    <button id=cluster data-key=clustersize data-default=up>Cluster Size</button>
+    <button id=size data-key=stepsize>Step Size</button>
+  </sort-sk>
+
+  <div id=stuffToBeSorted>
+    <pre data-clustersize=10  data-stepsize=1.2>Size=10   Step=1.2</pre>
+    <pre data-clustersize=50  data-stepsize=0.5>Size=50   Step=0.5</pre>
+    <pre data-clustersize=100 data-stepsize=0.6>Size=100  Step=0.6</pre>
+  </div>`;
+      const getValues = (name) => $('#stuffToBeSorted pre', container).map((ele) => +ele.dataset[name]);
+
+      const clusterButton = container.querySelector('#cluster');
+      const stepButton = container.querySelector('#size');
+
+      clusterButton.click();
+      assert.deepEqual([100, 50, 10], getValues('clustersize'), 'Defaults to up, so sort down on first click.');
+      clusterButton.click();
+      assert.deepEqual([10, 50, 100], getValues('clustersize'), 'Switch to up.');
+
+      stepButton.click();
+      assert.deepEqual([1.2, 0.6, 0.5], getValues('stepsize'), 'No default, so start sorting down.');
+      stepButton.click();
+      assert.deepEqual([0.5, 0.6, 1.2], getValues('stepsize'), 'Switch to up.');
+    })
+  });
+
+  it('sorts alphabetically with alpha attribute', function() {
+    return window.customElements.whenDefined('sort-sk').then(() => {
+      container.innerHTML = `
+  <sort-sk target=stuffToBeSorted2>
+    <button id=name  data-key=name data-default=down data-sort-type=alpha>Name</button>
+    <button id=level data-key=level                  data-sort-type=alpha>Level</button>
+  </sort-sk>
+
+  <div id=stuffToBeSorted2>
+    <pre data-name=foo data-level=alpha>foo alpha</pre>
+    <pre data-name=baz data-level=beta >baz beta</pre>
+    <pre data-name=bar data-level=gamma>bar gamma</pre>
+  </div>
+  `;
+      const getValues = (name) => $('#stuffToBeSorted2 pre', container).map((ele) => ele.dataset[name]);
+
+      const nameButton = container.querySelector('#name');
+      const levelButton = container.querySelector('#level');
+
+      nameButton.click();
+      assert.deepEqual(['bar', 'baz', 'foo'], getValues('name'), 'Defaults to down, so sort up.');
+      nameButton.click();
+      assert.deepEqual(['foo', 'baz', 'bar'], getValues('name'), 'Now switch to down.');
+
+      levelButton.click();
+      assert.deepEqual(['gamma', 'beta', 'alpha'], getValues('level'), 'No default, so sort down.');
+      levelButton.click();
+      assert.deepEqual(['alpha', 'beta', 'gamma'], getValues('level'), 'Now switch to up.');
+    })
+  });
+});