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