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.handleClickOutside = this.handleClickOutside.bind(this);
  59. this.handleClickOnToggle = this.handleClickOnToggle.bind(this);
  60. }
  61. /**
  62. * Initialise component.
  63. */
  64. init() {
  65. if (!ECL) {
  66. throw new TypeError('Called init but ECL is not present');
  67. }
  68. ECL.components = ECL.components || new Map();
  69. document.addEventListener('click', this.handleClickOutside);
  70. this.toggles = queryAll(this.toggleSelector, this.element);
  71. // Bind click event on toggles
  72. if (this.attachClickListener && this.toggles) {
  73. this.toggles.forEach((toggle) => {
  74. toggle.addEventListener('click', (event) =>
  75. this.handleClickOnToggle(event, toggle),
  76. );
  77. });
  78. }
  79. // Set ecl initialized attribute
  80. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  81. ECL.components.set(this.element, this);
  82. }
  83. /**
  84. * Register a callback function for a specific event.
  85. *
  86. * @param {string} eventName - The name of the event to listen for.
  87. * @param {Function} callback - The callback function to be invoked when the event occurs.
  88. * @returns {void}
  89. * @memberof Accordion
  90. * @instance
  91. *
  92. * @example
  93. * // Registering a callback for the 'click' event
  94. * accordion.on('onToggle', (event) => {
  95. * console.log('Toggle event occurred!', event);
  96. * });
  97. */
  98. on(eventName, callback) {
  99. this.eventManager.on(eventName, callback);
  100. }
  101. /**
  102. * Trigger a component event.
  103. *
  104. * @param {string} eventName - The name of the event to trigger.
  105. * @param {any} eventData - Data associated with the event.
  106. *
  107. * @memberof Accordion
  108. */
  109. trigger(eventName, eventData) {
  110. this.eventManager.trigger(eventName, eventData);
  111. }
  112. /**
  113. * Destroy component.
  114. */
  115. destroy() {
  116. document.removeEventListener('click', this.handleClickOutside);
  117. if (this.attachClickListener && this.toggles) {
  118. this.toggles.forEach((toggle) => {
  119. toggle.replaceWith(toggle.cloneNode(true));
  120. });
  121. }
  122. if (this.element) {
  123. this.element.removeAttribute('data-ecl-auto-initialized');
  124. ECL.components.delete(this.element);
  125. }
  126. }
  127. /**
  128. * @param {e} Event
  129. */
  130. handleClickOutside(e) {
  131. if (e.target && this.toggles && !this.element.contains(e.target)) {
  132. this.toggles.forEach((item) =>
  133. item.classList.remove('ecl-accordion__toggle--active'),
  134. );
  135. }
  136. }
  137. /**
  138. * @param {HTMLElement} toggle Target element to toggle.
  139. *
  140. * @fires Accordion#onToggle
  141. */
  142. handleClickOnToggle(event, toggle) {
  143. let isOpening = false;
  144. // Get target element
  145. const target = queryOne(
  146. `#${toggle.getAttribute('aria-controls')}`,
  147. this.element,
  148. );
  149. // Exit if no target found
  150. if (!target) {
  151. throw new TypeError(
  152. 'Target has to be provided for accordion (aria-controls)',
  153. );
  154. }
  155. // Get current status
  156. const isExpanded =
  157. this.forceClose === true ||
  158. toggle.getAttribute('aria-expanded') === 'true';
  159. // Toggle the expandable/collapsible
  160. toggle.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
  161. if (isExpanded) {
  162. target.hidden = true;
  163. } else {
  164. target.hidden = false;
  165. isOpening = true;
  166. }
  167. const eventData = { item: target, isOpening };
  168. this.trigger('onToggle', eventData);
  169. // Toggle icon
  170. const iconElement = queryOne(this.iconSelector, toggle);
  171. if (iconElement) {
  172. const useNode = queryOne('use', iconElement);
  173. if (useNode) {
  174. const originalXlinkHref = useNode.getAttribute('xlink:href');
  175. let newXlinkHref = '';
  176. if (isExpanded) {
  177. newXlinkHref = originalXlinkHref.replace('minus', 'plus');
  178. } else {
  179. newXlinkHref = originalXlinkHref.replace('plus', 'minus');
  180. }
  181. useNode.setAttribute('xlink:href', newXlinkHref);
  182. }
  183. }
  184. this.toggles.forEach((item) =>
  185. item.classList.remove('ecl-accordion__toggle--active'),
  186. );
  187. // This is the way we distinguish the click from a press on Enter
  188. if (event.detail > 0) {
  189. toggle.classList.add('ecl-accordion__toggle--active');
  190. }
  191. }
  192. }
  193. export default Accordion;