Chris@0
|
1 /**
|
Chris@0
|
2 * @file
|
Chris@0
|
3 * Polyfill for HTML5 details elements.
|
Chris@0
|
4 */
|
Chris@0
|
5
|
Chris@0
|
6 (function ($, Modernizr, Drupal) {
|
Chris@0
|
7 /**
|
Chris@0
|
8 * The collapsible details object represents a single details element.
|
Chris@0
|
9 *
|
Chris@0
|
10 * @constructor Drupal.CollapsibleDetails
|
Chris@0
|
11 *
|
Chris@0
|
12 * @param {HTMLElement} node
|
Chris@0
|
13 * The details element.
|
Chris@0
|
14 */
|
Chris@0
|
15 function CollapsibleDetails(node) {
|
Chris@0
|
16 this.$node = $(node);
|
Chris@0
|
17 this.$node.data('details', this);
|
Chris@0
|
18 // Expand details if there are errors inside, or if it contains an
|
Chris@0
|
19 // element that is targeted by the URI fragment identifier.
|
Chris@0
|
20 const anchor = location.hash && location.hash !== '#' ? `, ${location.hash}` : '';
|
Chris@0
|
21 if (this.$node.find(`.error${anchor}`).length) {
|
Chris@0
|
22 this.$node.attr('open', true);
|
Chris@0
|
23 }
|
Chris@0
|
24 // Initialize and setup the summary,
|
Chris@0
|
25 this.setupSummary();
|
Chris@0
|
26 // Initialize and setup the legend.
|
Chris@0
|
27 this.setupLegend();
|
Chris@0
|
28 }
|
Chris@0
|
29
|
Chris@0
|
30 $.extend(CollapsibleDetails, /** @lends Drupal.CollapsibleDetails */{
|
Chris@0
|
31
|
Chris@0
|
32 /**
|
Chris@0
|
33 * Holds references to instantiated CollapsibleDetails objects.
|
Chris@0
|
34 *
|
Chris@0
|
35 * @type {Array.<Drupal.CollapsibleDetails>}
|
Chris@0
|
36 */
|
Chris@0
|
37 instances: [],
|
Chris@0
|
38 });
|
Chris@0
|
39
|
Chris@0
|
40 $.extend(CollapsibleDetails.prototype, /** @lends Drupal.CollapsibleDetails# */{
|
Chris@0
|
41
|
Chris@0
|
42 /**
|
Chris@0
|
43 * Initialize and setup summary events and markup.
|
Chris@0
|
44 *
|
Chris@0
|
45 * @fires event:summaryUpdated
|
Chris@0
|
46 *
|
Chris@0
|
47 * @listens event:summaryUpdated
|
Chris@0
|
48 */
|
Chris@0
|
49 setupSummary() {
|
Chris@0
|
50 this.$summary = $('<span class="summary"></span>');
|
Chris@0
|
51 this.$node
|
Chris@0
|
52 .on('summaryUpdated', $.proxy(this.onSummaryUpdated, this))
|
Chris@0
|
53 .trigger('summaryUpdated');
|
Chris@0
|
54 },
|
Chris@0
|
55
|
Chris@0
|
56 /**
|
Chris@0
|
57 * Initialize and setup legend markup.
|
Chris@0
|
58 */
|
Chris@0
|
59 setupLegend() {
|
Chris@0
|
60 // Turn the summary into a clickable link.
|
Chris@0
|
61 const $legend = this.$node.find('> summary');
|
Chris@0
|
62
|
Chris@0
|
63 $('<span class="details-summary-prefix visually-hidden"></span>')
|
Chris@0
|
64 .append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show'))
|
Chris@0
|
65 .prependTo($legend)
|
Chris@0
|
66 .after(document.createTextNode(' '));
|
Chris@0
|
67
|
Chris@0
|
68 // .wrapInner() does not retain bound events.
|
Chris@0
|
69 $('<a class="details-title"></a>')
|
Chris@0
|
70 .attr('href', `#${this.$node.attr('id')}`)
|
Chris@0
|
71 .prepend($legend.contents())
|
Chris@0
|
72 .appendTo($legend);
|
Chris@0
|
73
|
Chris@0
|
74 $legend
|
Chris@0
|
75 .append(this.$summary)
|
Chris@0
|
76 .on('click', $.proxy(this.onLegendClick, this));
|
Chris@0
|
77 },
|
Chris@0
|
78
|
Chris@0
|
79 /**
|
Chris@0
|
80 * Handle legend clicks.
|
Chris@0
|
81 *
|
Chris@0
|
82 * @param {jQuery.Event} e
|
Chris@0
|
83 * The event triggered.
|
Chris@0
|
84 */
|
Chris@0
|
85 onLegendClick(e) {
|
Chris@0
|
86 this.toggle();
|
Chris@0
|
87 e.preventDefault();
|
Chris@0
|
88 },
|
Chris@0
|
89
|
Chris@0
|
90 /**
|
Chris@0
|
91 * Update summary.
|
Chris@0
|
92 */
|
Chris@0
|
93 onSummaryUpdated() {
|
Chris@0
|
94 const text = $.trim(this.$node.drupalGetSummary());
|
Chris@0
|
95 this.$summary.html(text ? ` (${text})` : '');
|
Chris@0
|
96 },
|
Chris@0
|
97
|
Chris@0
|
98 /**
|
Chris@0
|
99 * Toggle the visibility of a details element using smooth animations.
|
Chris@0
|
100 */
|
Chris@0
|
101 toggle() {
|
Chris@0
|
102 const isOpen = !!this.$node.attr('open');
|
Chris@0
|
103 const $summaryPrefix = this.$node.find('> summary span.details-summary-prefix');
|
Chris@0
|
104 if (isOpen) {
|
Chris@0
|
105 $summaryPrefix.html(Drupal.t('Show'));
|
Chris@0
|
106 }
|
Chris@0
|
107 else {
|
Chris@0
|
108 $summaryPrefix.html(Drupal.t('Hide'));
|
Chris@0
|
109 }
|
Chris@0
|
110 // Delay setting the attribute to emulate chrome behavior and make
|
Chris@0
|
111 // details-aria.js work as expected with this polyfill.
|
Chris@0
|
112 setTimeout(() => {
|
Chris@0
|
113 this.$node.attr('open', !isOpen);
|
Chris@0
|
114 }, 0);
|
Chris@0
|
115 },
|
Chris@0
|
116 });
|
Chris@0
|
117
|
Chris@0
|
118 /**
|
Chris@0
|
119 * Polyfill HTML5 details element.
|
Chris@0
|
120 *
|
Chris@0
|
121 * @type {Drupal~behavior}
|
Chris@0
|
122 *
|
Chris@0
|
123 * @prop {Drupal~behaviorAttach} attach
|
Chris@0
|
124 * Attaches behavior for the details element.
|
Chris@0
|
125 */
|
Chris@0
|
126 Drupal.behaviors.collapse = {
|
Chris@0
|
127 attach(context) {
|
Chris@0
|
128 if (Modernizr.details) {
|
Chris@0
|
129 return;
|
Chris@0
|
130 }
|
Chris@0
|
131 const $collapsibleDetails = $(context).find('details').once('collapse').addClass('collapse-processed');
|
Chris@0
|
132 if ($collapsibleDetails.length) {
|
Chris@0
|
133 for (let i = 0; i < $collapsibleDetails.length; i++) {
|
Chris@0
|
134 CollapsibleDetails.instances.push(new CollapsibleDetails($collapsibleDetails[i]));
|
Chris@0
|
135 }
|
Chris@0
|
136 }
|
Chris@0
|
137 },
|
Chris@0
|
138 };
|
Chris@0
|
139
|
Chris@0
|
140 /**
|
Chris@0
|
141 * Open parent details elements of a targeted page fragment.
|
Chris@0
|
142 *
|
Chris@0
|
143 * Opens all (nested) details element on a hash change or fragment link click
|
Chris@0
|
144 * when the target is a child element, in order to make sure the targeted
|
Chris@0
|
145 * element is visible. Aria attributes on the summary
|
Chris@0
|
146 * are set by triggering the click event listener in details-aria.js.
|
Chris@0
|
147 *
|
Chris@0
|
148 * @param {jQuery.Event} e
|
Chris@0
|
149 * The event triggered.
|
Chris@0
|
150 * @param {jQuery} $target
|
Chris@0
|
151 * The targeted node as a jQuery object.
|
Chris@0
|
152 */
|
Chris@0
|
153 const handleFragmentLinkClickOrHashChange = (e, $target) => {
|
Chris@0
|
154 $target.parents('details').not('[open]').find('> summary').trigger('click');
|
Chris@0
|
155 };
|
Chris@0
|
156
|
Chris@0
|
157 /**
|
Chris@0
|
158 * Binds a listener to handle fragment link clicks and URL hash changes.
|
Chris@0
|
159 */
|
Chris@0
|
160 $('body').on('formFragmentLinkClickOrHashChange.details', handleFragmentLinkClickOrHashChange);
|
Chris@0
|
161
|
Chris@0
|
162 // Expose constructor in the public space.
|
Chris@0
|
163 Drupal.CollapsibleDetails = CollapsibleDetails;
|
Chris@0
|
164 }(jQuery, Modernizr, Drupal));
|