accordion.js

  1. import { queryAll, queryOne } from '@ecl/dom-utils';
  2. import EventManager from '@ecl/event-manager';
  3. /**
  4. * @param {HTMLElement} element DOM element for component instantiation and scope
  5. * @param {Object} options
  6. * @param {String} options.toggleSelector Selector for toggling element
  7. * @param {String} options.iconSelector Selector for icon element
  8. * @param {Boolean} options.attachClickListener Whether or not to bind click events on toggle
  9. */
  10. export class Accordion {
  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 {Accordion} An instance of Accordion.
  18. */
  19. static autoInit(root, { ACCORDION: defaultOptions = {} } = {}) {
  20. const accordion = new Accordion(root, defaultOptions);
  21. accordion.init();
  22. root.ECLAccordion = accordion;
  23. return accordion;
  24. }
  25. /**
  26. * An array of supported events for this component.
  27. *
  28. * @type {Array<string>}
  29. * @event Accordion#onToggle
  30. * @memberof Accordion
  31. */
  32. supportedEvents = ['onToggle'];
  33. constructor(
  34. element,
  35. {
  36. toggleSelector = '[data-ecl-accordion-toggle]',
  37. iconSelector = '[data-ecl-accordion-icon]',
  38. attachClickListener = true,
  39. } = {},
  40. ) {
  41. // Check element
  42. if (!element || element.nodeType !== Node.ELEMENT_NODE) {
  43. throw new TypeError(
  44. 'DOM element should be given to initialize this widget.',
  45. );
  46. }
  47. this.element = element;
  48. this.eventManager = new EventManager();
  49. // Options
  50. this.toggleSelector = toggleSelector;
  51. this.iconSelector = iconSelector;
  52. this.attachClickListener = attachClickListener;
  53. // Private variables
  54. this.toggles = null;
  55. this.forceClose = false;
  56. this.target = null;
  57. // Bind `this` for use in callbacks
  58. this.handleClickOnToggle = this.handleClickOnToggle.bind(this);
  59. }
  60. /**
  61. * Initialise component.
  62. */
  63. init() {
  64. if (!ECL) {
  65. throw new TypeError('Called init but ECL is not present');
  66. }
  67. ECL.components = ECL.components || new Map();
  68. this.toggles = queryAll(this.toggleSelector, this.element);
  69. // Bind click event on toggles
  70. if (this.attachClickListener && this.toggles) {
  71. this.toggles.forEach((toggle) => {
  72. toggle.addEventListener('click', () =>
  73. this.handleClickOnToggle(toggle),
  74. );
  75. });
  76. }
  77. // Set ecl initialized attribute
  78. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  79. ECL.components.set(this.element, this);
  80. }
  81. /**
  82. * Register a callback function for a specific event.
  83. *
  84. * @param {string} eventName - The name of the event to listen for.
  85. * @param {Function} callback - The callback function to be invoked when the event occurs.
  86. * @returns {void}
  87. * @memberof Accordion
  88. * @instance
  89. *
  90. * @example
  91. * // Registering a callback for the 'click' event
  92. * accordion.on('onToggle', (event) => {
  93. * console.log('Toggle event occurred!', event);
  94. * });
  95. */
  96. on(eventName, callback) {
  97. this.eventManager.on(eventName, callback);
  98. }
  99. /**
  100. * Trigger a component event.
  101. *
  102. * @param {string} eventName - The name of the event to trigger.
  103. * @param {any} eventData - Data associated with the event.
  104. *
  105. * @memberof Accordion
  106. */
  107. trigger(eventName, eventData) {
  108. this.eventManager.trigger(eventName, eventData);
  109. }
  110. /**
  111. * Destroy component.
  112. */
  113. destroy() {
  114. if (this.attachClickListener && this.toggles) {
  115. this.toggles.forEach((toggle) => {
  116. toggle.replaceWith(toggle.cloneNode(true));
  117. });
  118. }
  119. if (this.element) {
  120. this.element.removeAttribute('data-ecl-auto-initialized');
  121. ECL.components.delete(this.element);
  122. }
  123. }
  124. /**
  125. * @param {HTMLElement} toggle Target element to toggle.
  126. *
  127. * @fires Accordion#onToggle
  128. */
  129. handleClickOnToggle(toggle) {
  130. let isOpening = false;
  131. // Get target element
  132. const target = queryOne(
  133. `#${toggle.getAttribute('aria-controls')}`,
  134. this.element,
  135. );
  136. // Exit if no target found
  137. if (!target) {
  138. throw new TypeError(
  139. 'Target has to be provided for accordion (aria-controls)',
  140. );
  141. }
  142. // Get current status
  143. const isExpanded =
  144. this.forceClose === true ||
  145. toggle.getAttribute('aria-expanded') === 'true';
  146. // Toggle the expandable/collapsible
  147. toggle.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
  148. if (isExpanded) {
  149. target.hidden = true;
  150. } else {
  151. target.hidden = false;
  152. isOpening = true;
  153. }
  154. const eventData = { item: target, isOpening };
  155. this.trigger('onToggle', eventData);
  156. }
  157. }
  158. export default Accordion;