blob: 80c2db0877652d87c5ea1adc83b17a3f2babd450 [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001'use strict';
2
3var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
4
5var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
6
7var _createClass2 = require('babel-runtime/helpers/createClass');
8
9var _createClass3 = _interopRequireDefault(_createClass2);
10
11var _constants = require('../utils/constants');
12
13var _stringUtils = require('../utils/string-utils');
14
15var _domUtils = require('../utils/dom-utils');
16
17function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
19var JS_COLLAPSIBLE = 'mdlext-js-collapsible'; /**
20 * @license
21 * Copyright 2016-2017 Leif Olsen. All Rights Reserved.
22 *
23 * Licensed under the Apache License, Version 2.0 (the "License");
24 * you may not use this file except in compliance with the License.
25 * You may obtain a copy of the License at
26 *
27 * http://www.apache.org/licenses/LICENSE-2.0
28 *
29 * Unless required by applicable law or agreed to in writing, software
30 * distributed under the License is distributed on an "AS IS" BASIS,
31 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32 * See the License for the specific language governing permissions and
33 * limitations under the License.
34 *
35 * This code is built with Google Material Design Lite,
36 * which is Licensed under the Apache License, Version 2.0
37 */
38
39/**
40 * A collapsible is a component to mark expandable and collapsible regions.
41 * The component use the aria-expanded state to indicate whether regions of
42 * the content are collapsible, and to expose whether a region is currently
43 * expanded or collapsed.
44 * @see https://www.w3.org/WAI/GL/wiki/Using_the_WAI-ARIA_aria-expanded_state_to_mark_expandable_and_collapsible_regions
45 */
46
47var COLLAPSIBLE_CONTROL_CLASS = 'mdlext-collapsible';
48var COLLAPSIBLE_GROUP_CLASS = 'mdlext-collapsible-group';
49var COLLAPSIBLE_REGION_CLASS = 'mdlext-collapsible-region';
50
51/**
52 * The collapsible component
53 */
54
55var Collapsible = function () {
56
57 /**
58 * @constructor
59 * @param {HTMLElement} element The element that this component is connected to.
60 */
61 function Collapsible(element) {
62 var _this = this;
63
64 (0, _classCallCheck3.default)(this, Collapsible);
65 this.element_ = null;
66 this.controlElement_ = null;
67
68 this.keyDownHandler = function (event) {
69 if (event.keyCode === _constants.VK_ENTER || event.keyCode === _constants.VK_SPACE) {
70 event.preventDefault();
71
72 // Trigger click
73 (event.target || _this.controlElement).dispatchEvent(new MouseEvent('click', {
74 bubbles: true,
75 cancelable: true,
76 view: window
77 }));
78 }
79 };
80
81 this.clickHandler = function (event) {
82 if (!_this.isDisabled) {
83 if (event.target !== _this.controlElement) {
84 // Do not toggle if a focusable element inside the control element triggered the event
85 var p = (0, _domUtils.getParentElements)(event.target, _this.controlElement);
86 p.push(event.target);
87 if (p.find(function (el) {
88 return (0, _domUtils.isFocusable)(el);
89 })) {
90 return;
91 }
92 }
93 _this.toggle();
94 }
95 };
96
97 this.element_ = element;
98 this.init();
99 }
100
101 (0, _createClass3.default)(Collapsible, [{
102 key: 'collapse',
103 value: function collapse() {
104 if (!this.isDisabled && this.isExpanded) {
105 if (this.dispatchToggleEvent('collapse')) {
106 this.controlElement.setAttribute('aria-expanded', 'false');
107 var regions = this.regionElements.slice(0);
108 for (var i = regions.length - 1; i >= 0; --i) {
109 regions[i].setAttribute('hidden', '');
110 }
111 }
112 }
113 }
114 }, {
115 key: 'expand',
116 value: function expand() {
117 if (!this.isDisabled && !this.isExpanded) {
118 if (this.dispatchToggleEvent('expand')) {
119 this.controlElement.setAttribute('aria-expanded', 'true');
120 this.regionElements.forEach(function (region) {
121 return region.removeAttribute('hidden');
122 });
123 }
124 }
125 }
126 }, {
127 key: 'toggle',
128 value: function toggle() {
129 if (this.isExpanded) {
130 this.collapse();
131 } else {
132 this.expand();
133 }
134 }
135 }, {
136 key: 'dispatchToggleEvent',
137 value: function dispatchToggleEvent(action) {
138 return this.element.dispatchEvent(new CustomEvent('toggle', {
139 bubbles: true,
140 cancelable: true,
141 detail: {
142 action: action
143 }
144 }));
145 }
146 }, {
147 key: 'disableToggle',
148 value: function disableToggle() {
149 this.controlElement.setAttribute('aria-disabled', true);
150 }
151 }, {
152 key: 'enableToggle',
153 value: function enableToggle() {
154 this.controlElement.removeAttribute('aria-disabled');
155 }
156 }, {
157 key: 'addRegionId',
158 value: function addRegionId(regionId) {
159 var ids = this.regionIds;
160 if (!ids.find(function (id) {
161 return regionId === id;
162 })) {
163 ids.push(regionId);
164 this.controlElement.setAttribute('aria-controls', ids.join(' '));
165 }
166 }
167 }, {
168 key: 'addRegionElement',
169 value: function addRegionElement(region) {
170 if (!(region.classList.contains(COLLAPSIBLE_GROUP_CLASS) || region.classList.contains(COLLAPSIBLE_REGION_CLASS))) {
171 region.classList.add(COLLAPSIBLE_GROUP_CLASS);
172 }
173
174 if (!region.hasAttribute('role')) {
175 var role = region.classList.contains(COLLAPSIBLE_GROUP_CLASS) ? 'group' : 'region';
176 region.setAttribute('role', role);
177 }
178
179 if (!region.hasAttribute('id')) {
180 region.id = region.getAttribute('role') + '-' + (0, _stringUtils.randomString)();
181 }
182
183 if (this.isExpanded) {
184 region.removeAttribute('hidden');
185 } else {
186 region.setAttribute('hidden', '');
187 }
188 this.addRegionId(region.id);
189 }
190 }, {
191 key: 'removeRegionElement',
192 value: function removeRegionElement(region) {
193 if (region && region.id) {
194 var ids = this.regionIds.filter(function (id) {
195 return id === region.id;
196 });
197 this.controlElement.setAttribute('aria-controls', ids.join(' '));
198 }
199 }
200 }, {
201 key: 'removeListeners',
202 value: function removeListeners() {
203 this.controlElement.removeEventListener('keydown', this.keyDownHandler);
204 this.controlElement.removeEventListener('click', this.clickHandler);
205 }
206 }, {
207 key: 'init',
208 value: function init() {
209 var _this2 = this;
210
211 var initControl = function initControl() {
212 // Find the button element
213 _this2.controlElement_ = _this2.element.querySelector('.' + COLLAPSIBLE_CONTROL_CLASS) || _this2.element;
214
215 // Add "aria-expanded" attribute if not present
216 if (!_this2.controlElement.hasAttribute('aria-expanded')) {
217 _this2.controlElement.setAttribute('aria-expanded', 'false');
218 }
219
220 // Add role=button if control != <button>
221 if (_this2.controlElement.nodeName.toLowerCase() !== 'button') {
222 _this2.controlElement.setAttribute('role', 'button');
223 }
224
225 // Add tabindex
226 if (!(0, _domUtils.isFocusable)(_this2.controlElement) && !_this2.controlElement.hasAttribute('tabindex')) {
227 _this2.controlElement.setAttribute('tabindex', '0');
228 }
229 };
230
231 var initRegions = function initRegions() {
232 var regions = [];
233 if (!_this2.controlElement.hasAttribute('aria-controls')) {
234 // Add siblings as collapsible region(s)
235 var r = _this2.element.nextElementSibling;
236 while (r) {
237 if (r.classList.contains(COLLAPSIBLE_GROUP_CLASS) || r.classList.contains(COLLAPSIBLE_REGION_CLASS)) {
238 regions.push(r);
239 } else if (r.classList.contains(JS_COLLAPSIBLE)) {
240 // A new collapsible component
241 break;
242 }
243 r = r.nextElementSibling;
244 }
245 } else {
246 regions = _this2.regionElements;
247 }
248 regions.forEach(function (region) {
249 return _this2.addRegionElement(region);
250 });
251 };
252
253 var addListeners = function addListeners() {
254 _this2.controlElement.addEventListener('keydown', _this2.keyDownHandler);
255 _this2.controlElement.addEventListener('click', _this2.clickHandler);
256 };
257
258 initControl();
259 initRegions();
260 this.removeListeners();
261 addListeners();
262 }
263 }, {
264 key: 'downgrade',
265 value: function downgrade() {
266 this.removeListeners();
267 }
268 }, {
269 key: 'element',
270 get: function get() {
271 return this.element_;
272 }
273 }, {
274 key: 'controlElement',
275 get: function get() {
276 return this.controlElement_;
277 }
278 }, {
279 key: 'isDisabled',
280 get: function get() {
281 return this.controlElement.hasAttribute('disabled') && this.controlElement.getAttribute('disabled').toLowerCase() !== 'false' || this.controlElement.hasAttribute('aria-disabled') && this.controlElement.getAttribute('aria-disabled').toLowerCase() !== 'false';
282 }
283 }, {
284 key: 'isExpanded',
285 get: function get() {
286 return this.controlElement.hasAttribute('aria-expanded') && this.controlElement.getAttribute('aria-expanded').toLowerCase() === 'true';
287 }
288 }, {
289 key: 'regionIds',
290 get: function get() {
291 return this.controlElement.hasAttribute('aria-controls') ? this.controlElement.getAttribute('aria-controls').split(' ') : [];
292 }
293 }, {
294 key: 'regionElements',
295 get: function get() {
296 return this.regionIds.map(function (id) {
297 return document.querySelector('#' + id);
298 }).filter(function (el) {
299 return el != null;
300 });
301 }
302 }]);
303 return Collapsible;
304}();
305
306(function () {
307 'use strict';
308
309 /**
310 * @constructor
311 * @param {HTMLElement} element The element that will be upgraded.
312 */
313
314 var MaterialExtCollapsible = function MaterialExtCollapsible(element) {
315 this.element_ = element;
316 this.collapsible = null;
317
318 // Initialize instance.
319 this.init();
320 };
321 window['MaterialExtCollapsible'] = MaterialExtCollapsible;
322
323 /**
324 * Initialize component
325 */
326 MaterialExtCollapsible.prototype.init = function () {
327 if (this.element_) {
328 this.collapsible = new Collapsible(this.element_);
329 this.element_.classList.add(_constants.IS_UPGRADED);
330
331 // Listen to 'mdl-componentdowngraded' event
332 this.element_.addEventListener('mdl-componentdowngraded', this.mdlDowngrade_.bind(this));
333 }
334 };
335
336 /*
337 * Downgrade component
338 * E.g remove listeners and clean up resources
339 */
340 MaterialExtCollapsible.prototype.mdlDowngrade_ = function () {
341 this.collapsible.downgrade();
342 };
343
344 // Public methods.
345
346 /**
347 * Get control element.
348 * @return {HTMLElement} element The element that controls the collapsible region.
349 * @public
350 */
351 MaterialExtCollapsible.prototype.getControlElement = function () {
352 return this.collapsible.controlElement;
353 };
354 MaterialExtCollapsible.prototype['getControlElement'] = MaterialExtCollapsible.prototype.getControlElement;
355
356 /**
357 * Get region elements controlled by this collapsible
358 * @returns {Array<HTMLElement>} the collapsible region elements
359 * @public
360 */
361 MaterialExtCollapsible.prototype.getRegionElements = function () {
362 return this.collapsible.regionElements;
363 };
364 MaterialExtCollapsible.prototype['getRegionElements'] = MaterialExtCollapsible.prototype.getRegionElements;
365
366 /**
367 * Add region elements.
368 * @param {Array<HTMLElement>} elements The element that will be upgraded.
369 * @return {void}
370 * @public
371 */
372 MaterialExtCollapsible.prototype.addRegionElements = function () {
373 var _this3 = this;
374
375 for (var _len = arguments.length, elements = Array(_len), _key = 0; _key < _len; _key++) {
376 elements[_key] = arguments[_key];
377 }
378
379 elements.forEach(function (element) {
380 return _this3.collapsible.addRegionElement(element);
381 });
382 };
383 MaterialExtCollapsible.prototype['addRegionElements'] = MaterialExtCollapsible.prototype.addRegionElements;
384
385 /**
386 * Remove collapsible region(s) from component.
387 * Note: This operation does not delete the element from the DOM tree.
388 * @param {Array<HTMLElement>} elements The element that will be upgraded.
389 * @public
390 */
391 MaterialExtCollapsible.prototype.removeRegionElements = function () {
392 var _this4 = this;
393
394 for (var _len2 = arguments.length, elements = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
395 elements[_key2] = arguments[_key2];
396 }
397
398 elements.forEach(function (element) {
399 return _this4.collapsible.removeRegionElement(element);
400 });
401 };
402 MaterialExtCollapsible.prototype['removeRegionElements'] = MaterialExtCollapsible.prototype.removeRegionElements;
403
404 /**
405 * Expand collapsible region(s)
406 * @return {void}
407 * @public
408 */
409 MaterialExtCollapsible.prototype.expand = function () {
410 this.collapsible.expand();
411 };
412 MaterialExtCollapsible.prototype['expand'] = MaterialExtCollapsible.prototype.expand;
413
414 /**
415 * Collapse collapsible region(s)
416 * @return {void}
417 * @public
418 */
419 MaterialExtCollapsible.prototype.collapse = function () {
420 this.collapsible.collapse();
421 };
422 MaterialExtCollapsible.prototype['collapse'] = MaterialExtCollapsible.prototype.collapse;
423
424 /**
425 * Toggle collapsible region(s)
426 * @return {void}
427 * @public
428 */
429 MaterialExtCollapsible.prototype.toggle = function () {
430 this.collapsible.toggle();
431 };
432 MaterialExtCollapsible.prototype['toggle'] = MaterialExtCollapsible.prototype.toggle;
433
434 /**
435 * Check whether component has aria-expanded state true
436 * @return {Boolean} true if aria-expanded="true", otherwise false
437 */
438 MaterialExtCollapsible.prototype.isExpanded = function () {
439 return this.collapsible.isExpanded;
440 };
441 MaterialExtCollapsible.prototype['isExpanded'] = MaterialExtCollapsible.prototype.isExpanded;
442
443 /**
444 * Check whether component has aria-disabled state set to true
445 * @return {Boolean} true if aria-disabled="true", otherwise false
446 */
447 MaterialExtCollapsible.prototype.isDisabled = function () {
448 return this.collapsible.isDisabled;
449 };
450 MaterialExtCollapsible.prototype['isDisabled'] = MaterialExtCollapsible.prototype.isDisabled;
451
452 /**
453 * Disables toggling of collapsible region(s)
454 * @return {void}
455 * @public
456 */
457 MaterialExtCollapsible.prototype.disableToggle = function () {
458 this.collapsible.disableToggle();
459 };
460 MaterialExtCollapsible.prototype['disableToggle'] = MaterialExtCollapsible.prototype.disableToggle;
461
462 /**
463 * Enables toggling of collapsible region(s)
464 * @return {void}
465 * @public
466 */
467 MaterialExtCollapsible.prototype.enableToggle = function () {
468 this.collapsible.enableToggle();
469 };
470 MaterialExtCollapsible.prototype['enableToggle'] = MaterialExtCollapsible.prototype.enableToggle;
471
472 // The component registers itself. It can assume componentHandler is available
473 // in the global scope.
474 /* eslint no-undef: 0 */
475 componentHandler.register({
476 constructor: MaterialExtCollapsible,
477 classAsString: 'MaterialExtCollapsible',
478 cssClass: JS_COLLAPSIBLE,
479 widget: true
480 });
481})();