collapse.es6.js 5.0 KB

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