table.js

  1. import { queryAll, queryOne } from '@ecl/dom-utils';
  2. import getSystem from '@ecl/builder/utils/getSystem';
  3. import iconSvgAllArrowEc from '@ecl/resources-ec-icons/dist/svg/all/solid-arrow.svg';
  4. import iconSvgAllArrowEu from '@ecl/resources-eu-icons/dist/svg/all/solid-arrow.svg';
  5. const system = getSystem();
  6. const iconSvgAllArrow = system === 'eu' ? iconSvgAllArrowEu : iconSvgAllArrowEc;
  7. const iconSvgAllArrowSize = system === 'eu' ? 'm' : 'xs';
  8. /**
  9. * @param {HTMLElement} element DOM element for component instantiation and scope
  10. * @param {Object} options
  11. * @param {String} options.sortSelector Selector for toggling element
  12. * @param {String} options.sortLabelSelector Selector for sorting button label
  13. * @param {Boolean} options.attachClickListener
  14. */
  15. export class Table {
  16. /**
  17. * @static
  18. * Shorthand for instance creation and initialisation.
  19. *
  20. * @param {HTMLElement} root DOM element for component instantiation and scope
  21. *
  22. * @return {Table} An instance of table.
  23. */
  24. static autoInit(root, { TABLE: defaultOptions = {} } = {}) {
  25. const table = new Table(root, defaultOptions);
  26. table.init();
  27. root.ECLTable = table;
  28. return table;
  29. }
  30. constructor(
  31. element,
  32. {
  33. sortSelector = '[data-ecl-table-sort-toggle]',
  34. sortLabelSelector = 'data-ecl-table-sort-label',
  35. } = {},
  36. ) {
  37. // Check element
  38. if (!element || element.nodeType !== Node.ELEMENT_NODE) {
  39. throw new TypeError(
  40. 'DOM element should be given to initialize this widget.',
  41. );
  42. }
  43. this.element = element;
  44. // Options
  45. this.sortSelector = sortSelector;
  46. this.sortLabelSelector = sortLabelSelector;
  47. // Private variables
  48. this.sortHeadings = null;
  49. // Bind `this` for use in callbacks
  50. this.handleClickOnSort = this.handleClickOnSort.bind(this);
  51. }
  52. /**
  53. * @returns {HTMLElement}
  54. */
  55. static createSortIcon(customClass) {
  56. const tempElement = document.createElement('span');
  57. tempElement.innerHTML = iconSvgAllArrow; // avoiding the use of not-so-stable createElementNs
  58. const svg = tempElement.children[0];
  59. svg.removeAttribute('height');
  60. svg.removeAttribute('width');
  61. svg.setAttribute('focusable', false);
  62. svg.setAttribute('aria-hidden', true);
  63. // The following element is <path> which does not support classList API as others.
  64. svg.setAttribute(
  65. 'class',
  66. `ecl-table__icon ecl-icon ecl-icon--${iconSvgAllArrowSize} ${customClass}`,
  67. );
  68. return svg;
  69. }
  70. /**
  71. * Initialise component.
  72. */
  73. init() {
  74. if (!ECL) {
  75. throw new TypeError('Called init but ECL is not present');
  76. }
  77. ECL.components = ECL.components || new Map();
  78. this.sortHeadings = queryAll(this.sortSelector, this.element);
  79. // Add sort arrows and bind click event on toggles.
  80. if (this.sortHeadings) {
  81. this.sortHeadings.forEach((tr) => {
  82. const sort = document.createElement('button');
  83. sort.classList.add('ecl-table__arrow');
  84. if (this.element.hasAttribute(this.sortLabelSelector)) {
  85. sort.setAttribute(
  86. 'aria-label',
  87. this.element.getAttribute(this.sortLabelSelector),
  88. );
  89. }
  90. sort.appendChild(Table.createSortIcon('ecl-table__icon-up'));
  91. sort.appendChild(Table.createSortIcon('ecl-table__icon-down'));
  92. tr.appendChild(sort);
  93. tr.addEventListener('click', (e) => this.handleClickOnSort(tr)(e));
  94. });
  95. }
  96. // Set default row order via dataset.
  97. const tbody = queryOne('tbody', this.element);
  98. [...queryAll('tr', tbody)].forEach((tr, index) => {
  99. tr.setAttribute('data-ecl-table-order', index);
  100. });
  101. // Set ecl initialized attribute
  102. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  103. ECL.components.set(this.element, this);
  104. }
  105. /**
  106. * Destroy component.
  107. */
  108. destroy() {
  109. if (this.sortHeadings) {
  110. this.sortHeadings.forEach((tr) => {
  111. tr.removeEventListener('click', (e) => this.handleClickOnSort(tr)(e));
  112. });
  113. }
  114. if (this.element) {
  115. this.element.removeAttribute('data-ecl-auto-initialized');
  116. ECL.components.delete(this.element);
  117. }
  118. }
  119. /**
  120. * @param {HTMLElement} toggle Target element to toggle.
  121. */
  122. handleClickOnSort = (toggle) => (event) => {
  123. event.preventDefault();
  124. const table = toggle.closest('table');
  125. const tbody = queryOne('tbody', table);
  126. let order = toggle.getAttribute('aria-sort');
  127. // Get current column index, taking into account the colspan.
  128. let colIndex = 0;
  129. let prev = toggle.previousElementSibling;
  130. while (prev) {
  131. colIndex += prev.getAttribute('colspan')
  132. ? Number(prev.getAttribute('colspan'))
  133. : 1;
  134. prev = prev.previousElementSibling;
  135. }
  136. // Cell comparer function.
  137. const comparer = (idx, asc) => (a, b) =>
  138. ((v1, v2) =>
  139. v1 !== '' && v2 !== '' && !Number.isNaN(+v1) && !Number.isNaN(+v2)
  140. ? v1 - v2
  141. : v1.toString().localeCompare(v2))(
  142. (asc ? a : b).children[idx].textContent,
  143. (asc ? b : a).children[idx].textContent,
  144. );
  145. if (order === 'descending') {
  146. // If current order is 'descending' reset column filter sort rows by default order.
  147. [...queryAll('tr', tbody)].forEach((tr, index) => {
  148. const defaultTr = queryOne(`[data-ecl-table-order='${index}']`);
  149. tbody.appendChild(defaultTr);
  150. });
  151. order = null;
  152. } else {
  153. // Otherwise we sort the rows and set new order.
  154. [...queryAll('tr', tbody)]
  155. .sort(comparer(colIndex, order !== 'ascending'))
  156. .forEach((tr) => tbody.appendChild(tr));
  157. order = order === 'ascending' ? 'descending' : 'ascending';
  158. }
  159. // Change heading aria-sort attr.
  160. this.sortHeadings.forEach((th) => {
  161. if (order && th === toggle) {
  162. th.setAttribute('aria-sort', order);
  163. } else {
  164. th.removeAttribute('aria-sort');
  165. }
  166. });
  167. };
  168. }
  169. export default Table;