category-filter.js

  1. import { queryAll } from '@ecl/dom-utils';
  2. /**
  3. * @param {HTMLElement} element DOM element for component instantiation and scope
  4. * @param {Object} options
  5. * @param {String} options.itemSelector Selector for the items
  6. * @param {String} options.parentItemSelector Selector for the parent items
  7. * @param {String} options.listSelector Selector for the lists
  8. * @param {Boolean} options.attachClickListener Whether or not to bind click events
  9. */
  10. export class CategoryFilter {
  11. /**
  12. * @static
  13. * Shorthand for instance creation and initialisation.
  14. *
  15. * @param {HTMLElement} root DOM element for component instantiation and scope
  16. *
  17. * @return {CategoryFilter} An instance of CategoryFilter.
  18. */
  19. static autoInit(root, { CATEGORY_FILTER: defaultOptions = {} } = {}) {
  20. const categoryFilter = new CategoryFilter(root, defaultOptions);
  21. categoryFilter.init();
  22. root.ECLCategoryFilter = categoryFilter;
  23. return categoryFilter;
  24. }
  25. constructor(
  26. element,
  27. {
  28. itemSelector = '.ecl-category-filter__item',
  29. parentItemSelector = 'ecl-category-filter__item--has-children',
  30. listSelector = '.ecl-category-filter__list',
  31. attachClickListener = true,
  32. } = {},
  33. ) {
  34. // Check element
  35. if (!element || element.nodeType !== Node.ELEMENT_NODE) {
  36. throw new TypeError(
  37. 'DOM element should be given to initialize this widget.',
  38. );
  39. }
  40. this.element = element;
  41. // Options
  42. this.itemSelector = itemSelector;
  43. this.parentItemSelector = parentItemSelector;
  44. this.listSelector = listSelector;
  45. this.attachClickListener = attachClickListener;
  46. // Private variables
  47. this.items = null;
  48. // Bind `this` for use in callbacks
  49. this.handleClickExpand = this.handleClickExpand.bind(this);
  50. this.expandParents = this.expandParents.bind(this);
  51. }
  52. /**
  53. * Initialise component.
  54. */
  55. init() {
  56. if (!ECL) {
  57. throw new TypeError('Called init but ECL is not present');
  58. }
  59. ECL.components = ECL.components || new Map();
  60. // Query elementslur
  61. this.items = queryAll(this.itemSelector, this.element);
  62. const e = { preventDefault: () => null };
  63. // Bind click event on open
  64. if (this.attachClickListener && this.items) {
  65. this.items.forEach((item) => {
  66. item.addEventListener('click', this.handleClickExpand);
  67. // Epand the needed items if there is a current item set
  68. if (item.getAttribute('aria-current')) {
  69. e.target = item;
  70. this.handleClickExpand(e);
  71. this.expandParents.call(this, item);
  72. }
  73. });
  74. }
  75. // Set ecl initialized attribute
  76. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  77. ECL.components.set(this.element, this);
  78. }
  79. /**
  80. * Destroy component.
  81. */
  82. destroy() {
  83. if (this.attachClickListener && this.items) {
  84. this.items.forEach((item) => {
  85. item.removeEventListener('click', this.handleClickExpand, false);
  86. });
  87. }
  88. if (this.element) {
  89. this.element.removeAttribute('data-ecl-auto-initialized');
  90. ECL.components.delete(this.element);
  91. }
  92. }
  93. /**
  94. * Expand parents of the given item.
  95. * @param {Node} item
  96. */
  97. expandParents(item) {
  98. if (!item) return;
  99. const e = { preventDefault: () => null };
  100. const parent = item.closest(this.listSelector)?.previousElementSibling;
  101. if (parent && parent.classList.contains(this.parentItemSelector)) {
  102. e.target = parent;
  103. this.handleClickExpand(e);
  104. this.expandParents.call(this, parent);
  105. }
  106. }
  107. /**
  108. * Expand tree list item.
  109. * @param {Event} e
  110. */
  111. handleClickExpand(e) {
  112. // Get item even if we clicked on the icon
  113. const treeItem = e.target.closest(this.itemSelector);
  114. const isNotInit = typeof e.stopPropagation === 'function';
  115. // Toggle current item
  116. if (isNotInit) {
  117. this.items.forEach((item) => {
  118. if (item === treeItem) {
  119. item.setAttribute('aria-current', true);
  120. } else {
  121. item.removeAttribute('aria-current');
  122. }
  123. });
  124. }
  125. // Toggle expanded
  126. const isExpanded = treeItem.getAttribute('aria-expanded');
  127. if (isExpanded && isExpanded === 'true') {
  128. e.preventDefault();
  129. treeItem.setAttribute('aria-expanded', 'false');
  130. treeItem.parentElement.classList.remove(
  131. 'ecl-category-filter__list-item--open',
  132. );
  133. } else if (isExpanded && isExpanded === 'false') {
  134. e.preventDefault();
  135. treeItem.setAttribute('aria-expanded', 'true');
  136. treeItem.parentElement.classList.add(
  137. 'ecl-category-filter__list-item--open',
  138. );
  139. }
  140. if (isExpanded && isNotInit) {
  141. // For first level, keep only one item open
  142. if (treeItem.classList.contains('ecl-category-filter__item--level-1')) {
  143. this.items.forEach((item) => {
  144. if (item !== treeItem) {
  145. item.parentElement.classList.remove(
  146. 'ecl-category-filter__list-item--open',
  147. );
  148. if (item.classList.contains(this.parentItemSelector)) {
  149. item.setAttribute('aria-expanded', 'false');
  150. }
  151. }
  152. });
  153. }
  154. }
  155. }
  156. }
  157. export default CategoryFilter;