expandable.js

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