timeline.js

  1. import { queryOne } from '@ecl/dom-utils';
  2. /**
  3. * @param {HTMLElement} element DOM element for component instantiation and scope
  4. * @param {Object} options
  5. * @param {String} options.buttonSelector
  6. * @param {String} options.labelSelector
  7. * @param {String} options.labelExpanded
  8. * @param {String} options.labelCollapsed
  9. * @param {Boolean} options.attachClickListener Whether or not to bind click events
  10. */
  11. export class Timeline {
  12. /**
  13. * @static
  14. * Shorthand for instance creation and initialisation.
  15. *
  16. * @param {HTMLElement} root DOM element for component instantiation and scope
  17. *
  18. * @return {Timeline} An instance of Timeline.
  19. */
  20. static autoInit(root, { TIMELINE: defaultOptions = {} } = {}) {
  21. const timeline = new Timeline(root, defaultOptions);
  22. timeline.init();
  23. root.ECLTimeline = timeline;
  24. return timeline;
  25. }
  26. constructor(
  27. element,
  28. {
  29. buttonSelector = '[data-ecl-timeline-button]',
  30. labelSelector = '[data-ecl-label]',
  31. labelExpanded = 'data-ecl-label-expanded',
  32. labelCollapsed = 'data-ecl-label-collapsed',
  33. attachClickListener = true,
  34. } = {},
  35. ) {
  36. // Check element
  37. if (!element || element.nodeType !== Node.ELEMENT_NODE) {
  38. throw new TypeError(
  39. 'DOM element should be given to initialize this widget.',
  40. );
  41. }
  42. this.element = element;
  43. // Options
  44. this.buttonSelector = buttonSelector;
  45. this.labelSelector = labelSelector;
  46. this.labelExpanded = labelExpanded;
  47. this.labelCollapsed = labelCollapsed;
  48. this.attachClickListener = attachClickListener;
  49. // Private variables
  50. this.button = null;
  51. this.label = null;
  52. // Bind `this` for use in callbacks
  53. this.handleClickOnButton = this.handleClickOnButton.bind(this);
  54. }
  55. /**
  56. * Initialise component.
  57. */
  58. init() {
  59. if (!ECL) {
  60. throw new TypeError('Called init but ECL is not present');
  61. }
  62. ECL.components = ECL.components || new Map();
  63. // Query elements
  64. this.button = queryOne(this.buttonSelector, this.element);
  65. // Get label, if any
  66. this.label = queryOne(this.labelSelector, this.element);
  67. // Bind click event on button
  68. if (this.attachClickListener && this.button) {
  69. this.button.addEventListener('click', this.handleClickOnButton);
  70. }
  71. // Set ecl initialized attribute
  72. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  73. ECL.components.set(this.element, this);
  74. }
  75. /**
  76. * Destroy component.
  77. */
  78. destroy() {
  79. if (this.attachClickListener && this.button) {
  80. this.button.removeEventListener('click', this.handleClickOnButton);
  81. }
  82. if (this.element) {
  83. this.element.removeAttribute('data-ecl-auto-initialized');
  84. ECL.components.delete(this.element);
  85. }
  86. }
  87. /**
  88. * Expand timeline if not such already.
  89. */
  90. handleClickOnButton() {
  91. // Get current status
  92. const isExpanded = this.button.getAttribute('aria-expanded') === 'true';
  93. // Toggle the expandable/collapsible
  94. this.button.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
  95. if (isExpanded) {
  96. this.element.removeAttribute('data-ecl-timeline-expanded');
  97. // Scroll up to the button
  98. this.button.blur();
  99. this.button.focus();
  100. } else {
  101. this.element.setAttribute('data-ecl-timeline-expanded', 'true');
  102. }
  103. // Toggle label if possible
  104. if (
  105. this.label &&
  106. !isExpanded &&
  107. this.button.hasAttribute(this.labelExpanded)
  108. ) {
  109. this.label.innerHTML = this.button.getAttribute(this.labelExpanded);
  110. } else if (
  111. this.label &&
  112. isExpanded &&
  113. this.button.hasAttribute(this.labelCollapsed)
  114. ) {
  115. this.label.innerHTML = this.button.getAttribute(this.labelCollapsed);
  116. }
  117. return this;
  118. }
  119. }
  120. export default Timeline;