/** * @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 = window.location.hash && window.location.hash !== '#' ? `, ${window.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.} */ instances: [], }, ); $.extend( CollapsibleDetails.prototype, /** @lends Drupal.CollapsibleDetails# */ { /** * Initialize and setup summary events and markup. * * @fires event:summaryUpdated * * @listens event:summaryUpdated */ setupSummary() { this.$summary = $(''); 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'); $('') .append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show')) .prependTo($legend) .after(document.createTextNode(' ')); // .wrapInner() does not retain bound events. $('') .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);