summaryrefslogtreecommitdiffstatshomepage
path: root/core/misc/collapse.js
diff options
context:
space:
mode:
Diffstat (limited to 'core/misc/collapse.js')
-rw-r--r--core/misc/collapse.js163
1 files changed, 122 insertions, 41 deletions
diff --git a/core/misc/collapse.js b/core/misc/collapse.js
index 32193ea1730..f2e07b65329 100644
--- a/core/misc/collapse.js
+++ b/core/misc/collapse.js
@@ -1,74 +1,155 @@
/**
-* DO NOT EDIT THIS FILE.
-* See the following change record for more information,
-* https://www.drupal.org/node/2815083
-* @preserve
-**/
+ * @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);
- const anchor = window.location.hash && window.location.hash !== '#' ? `, ${window.location.hash}` : '';
-
+ // 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 set up the summary polyfill.
this.setupSummaryPolyfill();
}
- $.extend(CollapsibleDetails, {
- instances: []
- });
- $.extend(CollapsibleDetails.prototype, {
- setupSummaryPolyfill() {
- const $summary = this.$node.find('> summary');
- $summary.attr('tabindex', '-1');
- $('<span class="details-summary-prefix visually-hidden"></span>').append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show')).prependTo($summary).after(document.createTextNode(' '));
- $('<a class="details-title"></a>').attr('href', `#${this.$node.attr('id')}`).prepend($summary.contents()).appendTo($summary);
- $summary.append(this.$summary).on('click', $.proxy(this.onSummaryClick, this));
+ $.extend(
+ CollapsibleDetails,
+ /** @lends Drupal.CollapsibleDetails */ {
+ /**
+ * Holds references to instantiated CollapsibleDetails objects.
+ *
+ * @type {Array.<Drupal.CollapsibleDetails>}
+ */
+ instances: [],
},
+ );
- onSummaryClick(e) {
- this.toggle();
- e.preventDefault();
- },
+ $.extend(
+ CollapsibleDetails.prototype,
+ /** @lends Drupal.CollapsibleDetails# */ {
+ /**
+ * Initialize and setup summary markup.
+ */
+ setupSummaryPolyfill() {
+ // Turn the summary into a clickable link.
+ const $summary = this.$node.find('> summary');
- toggle() {
- const isOpen = !!this.$node.attr('open');
- const $summaryPrefix = this.$node.find('> summary span.details-summary-prefix');
+ // If this polyfill is in use, the browser does not recognize
+ // <summary> as a focusable element. The tabindex is set to -1 so the
+ // tabbable library does not incorrectly identify it as tabbable.
+ $summary.attr('tabindex', '-1');
- if (isOpen) {
- $summaryPrefix.html(Drupal.t('Show'));
- } else {
- $summaryPrefix.html(Drupal.t('Hide'));
- }
+ $('<span class="details-summary-prefix visually-hidden"></span>')
+ .append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show'))
+ .prependTo($summary)
+ .after(document.createTextNode(' '));
- setTimeout(() => {
- this.$node.attr('open', !isOpen);
- }, 0);
- }
+ // .wrapInner() does not retain bound events.
+ $('<a class="details-title"></a>')
+ .attr('href', `#${this.$node.attr('id')}`)
+ .prepend($summary.contents())
+ .appendTo($summary);
+
+ $summary
+ .append(this.$summary)
+ .on('click', $.proxy(this.onSummaryClick, this));
+ },
+
+ /**
+ * Handle summary clicks.
+ *
+ * @param {jQuery.Event} e
+ * The event triggered.
+ */
+ onSummaryClick(e) {
+ this.toggle();
+ e.preventDefault();
+ },
- });
+ /**
+ * 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;
}
-
- once('collapse', 'details', context).forEach(detail => {
+ once('collapse', 'details', context).forEach((detail) => {
+ // This class is used for styling purpose only.
detail.classList.add('collapse-processed');
CollapsibleDetails.instances.push(new CollapsibleDetails(detail));
});
- }
-
+ },
};
+ /**
+ * 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');
};
- $('body').on('formFragmentLinkClickOrHashChange.details', handleFragmentLinkClickOrHashChange);
+ /**
+ * 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); \ No newline at end of file
+})(jQuery, Modernizr, Drupal);