collapse.es6.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. /**
  2. * @file
  3. * Polyfill for HTML5 details elements.
  4. */
  5. (function ($, Modernizr, Drupal) {
  6. /**
  7. * The collapsible details object represents a single details element.
  8. *
  9. * @constructor Drupal.CollapsibleDetails
  10. *
  11. * @param {HTMLElement} node
  12. * The details element.
  13. */
  14. function CollapsibleDetails(node) {
  15. this.$node = $(node);
  16. this.$node.data('details', this);
  17. // Expand details if there are errors inside, or if it contains an
  18. // element that is targeted by the URI fragment identifier.
  19. const anchor = location.hash && location.hash !== '#' ? `, ${location.hash}` : '';
  20. if (this.$node.find(`.error${anchor}`).length) {
  21. this.$node.attr('open', true);
  22. }
  23. // Initialize and setup the summary,
  24. this.setupSummary();
  25. // Initialize and setup the legend.
  26. this.setupLegend();
  27. }
  28. $.extend(CollapsibleDetails, /** @lends Drupal.CollapsibleDetails */{
  29. /**
  30. * Holds references to instantiated CollapsibleDetails objects.
  31. *
  32. * @type {Array.<Drupal.CollapsibleDetails>}
  33. */
  34. instances: [],
  35. });
  36. $.extend(CollapsibleDetails.prototype, /** @lends Drupal.CollapsibleDetails# */{
  37. /**
  38. * Initialize and setup summary events and markup.
  39. *
  40. * @fires event:summaryUpdated
  41. *
  42. * @listens event:summaryUpdated
  43. */
  44. setupSummary() {
  45. this.$summary = $('<span class="summary"></span>');
  46. this.$node
  47. .on('summaryUpdated', $.proxy(this.onSummaryUpdated, this))
  48. .trigger('summaryUpdated');
  49. },
  50. /**
  51. * Initialize and setup legend markup.
  52. */
  53. setupLegend() {
  54. // Turn the summary into a clickable link.
  55. const $legend = this.$node.find('> summary');
  56. $('<span class="details-summary-prefix visually-hidden"></span>')
  57. .append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show'))
  58. .prependTo($legend)
  59. .after(document.createTextNode(' '));
  60. // .wrapInner() does not retain bound events.
  61. $('<a class="details-title"></a>')
  62. .attr('href', `#${this.$node.attr('id')}`)
  63. .prepend($legend.contents())
  64. .appendTo($legend);
  65. $legend
  66. .append(this.$summary)
  67. .on('click', $.proxy(this.onLegendClick, this));
  68. },
  69. /**
  70. * Handle legend clicks.
  71. *
  72. * @param {jQuery.Event} e
  73. * The event triggered.
  74. */
  75. onLegendClick(e) {
  76. this.toggle();
  77. e.preventDefault();
  78. },
  79. /**
  80. * Update summary.
  81. */
  82. onSummaryUpdated() {
  83. const text = $.trim(this.$node.drupalGetSummary());
  84. this.$summary.html(text ? ` (${text})` : '');
  85. },
  86. /**
  87. * Toggle the visibility of a details element using smooth animations.
  88. */
  89. toggle() {
  90. const isOpen = !!this.$node.attr('open');
  91. const $summaryPrefix = this.$node.find('> summary span.details-summary-prefix');
  92. if (isOpen) {
  93. $summaryPrefix.html(Drupal.t('Show'));
  94. }
  95. else {
  96. $summaryPrefix.html(Drupal.t('Hide'));
  97. }
  98. // Delay setting the attribute to emulate chrome behavior and make
  99. // details-aria.js work as expected with this polyfill.
  100. setTimeout(() => {
  101. this.$node.attr('open', !isOpen);
  102. }, 0);
  103. },
  104. });
  105. /**
  106. * Polyfill HTML5 details element.
  107. *
  108. * @type {Drupal~behavior}
  109. *
  110. * @prop {Drupal~behaviorAttach} attach
  111. * Attaches behavior for the details element.
  112. */
  113. Drupal.behaviors.collapse = {
  114. attach(context) {
  115. if (Modernizr.details) {
  116. return;
  117. }
  118. const $collapsibleDetails = $(context).find('details').once('collapse').addClass('collapse-processed');
  119. if ($collapsibleDetails.length) {
  120. for (let i = 0; i < $collapsibleDetails.length; i++) {
  121. CollapsibleDetails.instances.push(new CollapsibleDetails($collapsibleDetails[i]));
  122. }
  123. }
  124. },
  125. };
  126. /**
  127. * Open parent details elements of a targeted page fragment.
  128. *
  129. * Opens all (nested) details element on a hash change or fragment link click
  130. * when the target is a child element, in order to make sure the targeted
  131. * element is visible. Aria attributes on the summary
  132. * are set by triggering the click event listener in details-aria.js.
  133. *
  134. * @param {jQuery.Event} e
  135. * The event triggered.
  136. * @param {jQuery} $target
  137. * The targeted node as a jQuery object.
  138. */
  139. const handleFragmentLinkClickOrHashChange = (e, $target) => {
  140. $target.parents('details').not('[open]').find('> summary').trigger('click');
  141. };
  142. /**
  143. * Binds a listener to handle fragment link clicks and URL hash changes.
  144. */
  145. $('body').on('formFragmentLinkClickOrHashChange.details', handleFragmentLinkClickOrHashChange);
  146. // Expose constructor in the public space.
  147. Drupal.CollapsibleDetails = CollapsibleDetails;
  148. }(jQuery, Modernizr, Drupal));