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