--- /dev/null
+/**
+ * @file
+ * Polyfill for HTML5 details elements.
+ */
+
+(function ($, Modernizr, Drupal) {
+ /**
+ * The collapsible details object represents a single details element.
+ *
+ * @constructor Drupal.CollapsibleDetails
+ *
+ * @param {HTMLElement} node
+ * The details element.
+ */
+ function CollapsibleDetails(node) {
+ this.$node = $(node);
+ this.$node.data('details', this);
+ // Expand details if there are errors inside, or if it contains an
+ // element that is targeted by the URI fragment identifier.
+ const anchor = location.hash && location.hash !== '#' ? `, ${location.hash}` : '';
+ if (this.$node.find(`.error${anchor}`).length) {
+ this.$node.attr('open', true);
+ }
+ // Initialize and setup the summary,
+ this.setupSummary();
+ // Initialize and setup the legend.
+ this.setupLegend();
+ }
+
+ $.extend(CollapsibleDetails, /** @lends Drupal.CollapsibleDetails */{
+
+ /**
+ * Holds references to instantiated CollapsibleDetails objects.
+ *
+ * @type {Array.<Drupal.CollapsibleDetails>}
+ */
+ instances: [],
+ });
+
+ $.extend(CollapsibleDetails.prototype, /** @lends Drupal.CollapsibleDetails# */{
+
+ /**
+ * Initialize and setup summary events and markup.
+ *
+ * @fires event:summaryUpdated
+ *
+ * @listens event:summaryUpdated
+ */
+ setupSummary() {
+ this.$summary = $('<span class="summary"></span>');
+ this.$node
+ .on('summaryUpdated', $.proxy(this.onSummaryUpdated, this))
+ .trigger('summaryUpdated');
+ },
+
+ /**
+ * Initialize and setup legend markup.
+ */
+ setupLegend() {
+ // Turn the summary into a clickable link.
+ const $legend = this.$node.find('> summary');
+
+ $('<span class="details-summary-prefix visually-hidden"></span>')
+ .append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show'))
+ .prependTo($legend)
+ .after(document.createTextNode(' '));
+
+ // .wrapInner() does not retain bound events.
+ $('<a class="details-title"></a>')
+ .attr('href', `#${this.$node.attr('id')}`)
+ .prepend($legend.contents())
+ .appendTo($legend);
+
+ $legend
+ .append(this.$summary)
+ .on('click', $.proxy(this.onLegendClick, this));
+ },
+
+ /**
+ * Handle legend clicks.
+ *
+ * @param {jQuery.Event} e
+ * The event triggered.
+ */
+ onLegendClick(e) {
+ this.toggle();
+ e.preventDefault();
+ },
+
+ /**
+ * Update summary.
+ */
+ onSummaryUpdated() {
+ const text = $.trim(this.$node.drupalGetSummary());
+ this.$summary.html(text ? ` (${text})` : '');
+ },
+
+ /**
+ * Toggle the visibility of a details element using smooth animations.
+ */
+ toggle() {
+ const isOpen = !!this.$node.attr('open');
+ const $summaryPrefix = this.$node.find('> summary span.details-summary-prefix');
+ if (isOpen) {
+ $summaryPrefix.html(Drupal.t('Show'));
+ }
+ else {
+ $summaryPrefix.html(Drupal.t('Hide'));
+ }
+ // Delay setting the attribute to emulate chrome behavior and make
+ // details-aria.js work as expected with this polyfill.
+ setTimeout(() => {
+ this.$node.attr('open', !isOpen);
+ }, 0);
+ },
+ });
+
+ /**
+ * Polyfill HTML5 details element.
+ *
+ * @type {Drupal~behavior}
+ *
+ * @prop {Drupal~behaviorAttach} attach
+ * Attaches behavior for the details element.
+ */
+ Drupal.behaviors.collapse = {
+ attach(context) {
+ if (Modernizr.details) {
+ return;
+ }
+ const $collapsibleDetails = $(context).find('details').once('collapse').addClass('collapse-processed');
+ if ($collapsibleDetails.length) {
+ for (let i = 0; i < $collapsibleDetails.length; i++) {
+ CollapsibleDetails.instances.push(new CollapsibleDetails($collapsibleDetails[i]));
+ }
+ }
+ },
+ };
+
+ /**
+ * Open parent details elements of a targeted page fragment.
+ *
+ * Opens all (nested) details element on a hash change or fragment link click
+ * when the target is a child element, in order to make sure the targeted
+ * element is visible. Aria attributes on the summary
+ * are set by triggering the click event listener in details-aria.js.
+ *
+ * @param {jQuery.Event} e
+ * The event triggered.
+ * @param {jQuery} $target
+ * The targeted node as a jQuery object.
+ */
+ const handleFragmentLinkClickOrHashChange = (e, $target) => {
+ $target.parents('details').not('[open]').find('> summary').trigger('click');
+ };
+
+ /**
+ * Binds a listener to handle fragment link clicks and URL hash changes.
+ */
+ $('body').on('formFragmentLinkClickOrHashChange.details', handleFragmentLinkClickOrHashChange);
+
+ // Expose constructor in the public space.
+ Drupal.CollapsibleDetails = CollapsibleDetails;
+}(jQuery, Modernizr, Drupal));