table.js

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