/**
* @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.}
*/
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));