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