blob: 80c2db0877652d87c5ea1adc83b17a3f2babd450 [file] [log] [blame]
'use strict';
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _constants = require('../utils/constants');
var _stringUtils = require('../utils/string-utils');
var _domUtils = require('../utils/dom-utils');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var JS_COLLAPSIBLE = 'mdlext-js-collapsible'; /**
* @license
* Copyright 2016-2017 Leif Olsen. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This code is built with Google Material Design Lite,
* which is Licensed under the Apache License, Version 2.0
*/
/**
* A collapsible is a component to mark expandable and collapsible regions.
* The component use the aria-expanded state to indicate whether regions of
* the content are collapsible, and to expose whether a region is currently
* expanded or collapsed.
* @see https://www.w3.org/WAI/GL/wiki/Using_the_WAI-ARIA_aria-expanded_state_to_mark_expandable_and_collapsible_regions
*/
var COLLAPSIBLE_CONTROL_CLASS = 'mdlext-collapsible';
var COLLAPSIBLE_GROUP_CLASS = 'mdlext-collapsible-group';
var COLLAPSIBLE_REGION_CLASS = 'mdlext-collapsible-region';
/**
* The collapsible component
*/
var Collapsible = function () {
/**
* @constructor
* @param {HTMLElement} element The element that this component is connected to.
*/
function Collapsible(element) {
var _this = this;
(0, _classCallCheck3.default)(this, Collapsible);
this.element_ = null;
this.controlElement_ = null;
this.keyDownHandler = function (event) {
if (event.keyCode === _constants.VK_ENTER || event.keyCode === _constants.VK_SPACE) {
event.preventDefault();
// Trigger click
(event.target || _this.controlElement).dispatchEvent(new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
}));
}
};
this.clickHandler = function (event) {
if (!_this.isDisabled) {
if (event.target !== _this.controlElement) {
// Do not toggle if a focusable element inside the control element triggered the event
var p = (0, _domUtils.getParentElements)(event.target, _this.controlElement);
p.push(event.target);
if (p.find(function (el) {
return (0, _domUtils.isFocusable)(el);
})) {
return;
}
}
_this.toggle();
}
};
this.element_ = element;
this.init();
}
(0, _createClass3.default)(Collapsible, [{
key: 'collapse',
value: function collapse() {
if (!this.isDisabled && this.isExpanded) {
if (this.dispatchToggleEvent('collapse')) {
this.controlElement.setAttribute('aria-expanded', 'false');
var regions = this.regionElements.slice(0);
for (var i = regions.length - 1; i >= 0; --i) {
regions[i].setAttribute('hidden', '');
}
}
}
}
}, {
key: 'expand',
value: function expand() {
if (!this.isDisabled && !this.isExpanded) {
if (this.dispatchToggleEvent('expand')) {
this.controlElement.setAttribute('aria-expanded', 'true');
this.regionElements.forEach(function (region) {
return region.removeAttribute('hidden');
});
}
}
}
}, {
key: 'toggle',
value: function toggle() {
if (this.isExpanded) {
this.collapse();
} else {
this.expand();
}
}
}, {
key: 'dispatchToggleEvent',
value: function dispatchToggleEvent(action) {
return this.element.dispatchEvent(new CustomEvent('toggle', {
bubbles: true,
cancelable: true,
detail: {
action: action
}
}));
}
}, {
key: 'disableToggle',
value: function disableToggle() {
this.controlElement.setAttribute('aria-disabled', true);
}
}, {
key: 'enableToggle',
value: function enableToggle() {
this.controlElement.removeAttribute('aria-disabled');
}
}, {
key: 'addRegionId',
value: function addRegionId(regionId) {
var ids = this.regionIds;
if (!ids.find(function (id) {
return regionId === id;
})) {
ids.push(regionId);
this.controlElement.setAttribute('aria-controls', ids.join(' '));
}
}
}, {
key: 'addRegionElement',
value: function addRegionElement(region) {
if (!(region.classList.contains(COLLAPSIBLE_GROUP_CLASS) || region.classList.contains(COLLAPSIBLE_REGION_CLASS))) {
region.classList.add(COLLAPSIBLE_GROUP_CLASS);
}
if (!region.hasAttribute('role')) {
var role = region.classList.contains(COLLAPSIBLE_GROUP_CLASS) ? 'group' : 'region';
region.setAttribute('role', role);
}
if (!region.hasAttribute('id')) {
region.id = region.getAttribute('role') + '-' + (0, _stringUtils.randomString)();
}
if (this.isExpanded) {
region.removeAttribute('hidden');
} else {
region.setAttribute('hidden', '');
}
this.addRegionId(region.id);
}
}, {
key: 'removeRegionElement',
value: function removeRegionElement(region) {
if (region && region.id) {
var ids = this.regionIds.filter(function (id) {
return id === region.id;
});
this.controlElement.setAttribute('aria-controls', ids.join(' '));
}
}
}, {
key: 'removeListeners',
value: function removeListeners() {
this.controlElement.removeEventListener('keydown', this.keyDownHandler);
this.controlElement.removeEventListener('click', this.clickHandler);
}
}, {
key: 'init',
value: function init() {
var _this2 = this;
var initControl = function initControl() {
// Find the button element
_this2.controlElement_ = _this2.element.querySelector('.' + COLLAPSIBLE_CONTROL_CLASS) || _this2.element;
// Add "aria-expanded" attribute if not present
if (!_this2.controlElement.hasAttribute('aria-expanded')) {
_this2.controlElement.setAttribute('aria-expanded', 'false');
}
// Add role=button if control != <button>
if (_this2.controlElement.nodeName.toLowerCase() !== 'button') {
_this2.controlElement.setAttribute('role', 'button');
}
// Add tabindex
if (!(0, _domUtils.isFocusable)(_this2.controlElement) && !_this2.controlElement.hasAttribute('tabindex')) {
_this2.controlElement.setAttribute('tabindex', '0');
}
};
var initRegions = function initRegions() {
var regions = [];
if (!_this2.controlElement.hasAttribute('aria-controls')) {
// Add siblings as collapsible region(s)
var r = _this2.element.nextElementSibling;
while (r) {
if (r.classList.contains(COLLAPSIBLE_GROUP_CLASS) || r.classList.contains(COLLAPSIBLE_REGION_CLASS)) {
regions.push(r);
} else if (r.classList.contains(JS_COLLAPSIBLE)) {
// A new collapsible component
break;
}
r = r.nextElementSibling;
}
} else {
regions = _this2.regionElements;
}
regions.forEach(function (region) {
return _this2.addRegionElement(region);
});
};
var addListeners = function addListeners() {
_this2.controlElement.addEventListener('keydown', _this2.keyDownHandler);
_this2.controlElement.addEventListener('click', _this2.clickHandler);
};
initControl();
initRegions();
this.removeListeners();
addListeners();
}
}, {
key: 'downgrade',
value: function downgrade() {
this.removeListeners();
}
}, {
key: 'element',
get: function get() {
return this.element_;
}
}, {
key: 'controlElement',
get: function get() {
return this.controlElement_;
}
}, {
key: 'isDisabled',
get: function get() {
return this.controlElement.hasAttribute('disabled') && this.controlElement.getAttribute('disabled').toLowerCase() !== 'false' || this.controlElement.hasAttribute('aria-disabled') && this.controlElement.getAttribute('aria-disabled').toLowerCase() !== 'false';
}
}, {
key: 'isExpanded',
get: function get() {
return this.controlElement.hasAttribute('aria-expanded') && this.controlElement.getAttribute('aria-expanded').toLowerCase() === 'true';
}
}, {
key: 'regionIds',
get: function get() {
return this.controlElement.hasAttribute('aria-controls') ? this.controlElement.getAttribute('aria-controls').split(' ') : [];
}
}, {
key: 'regionElements',
get: function get() {
return this.regionIds.map(function (id) {
return document.querySelector('#' + id);
}).filter(function (el) {
return el != null;
});
}
}]);
return Collapsible;
}();
(function () {
'use strict';
/**
* @constructor
* @param {HTMLElement} element The element that will be upgraded.
*/
var MaterialExtCollapsible = function MaterialExtCollapsible(element) {
this.element_ = element;
this.collapsible = null;
// Initialize instance.
this.init();
};
window['MaterialExtCollapsible'] = MaterialExtCollapsible;
/**
* Initialize component
*/
MaterialExtCollapsible.prototype.init = function () {
if (this.element_) {
this.collapsible = new Collapsible(this.element_);
this.element_.classList.add(_constants.IS_UPGRADED);
// Listen to 'mdl-componentdowngraded' event
this.element_.addEventListener('mdl-componentdowngraded', this.mdlDowngrade_.bind(this));
}
};
/*
* Downgrade component
* E.g remove listeners and clean up resources
*/
MaterialExtCollapsible.prototype.mdlDowngrade_ = function () {
this.collapsible.downgrade();
};
// Public methods.
/**
* Get control element.
* @return {HTMLElement} element The element that controls the collapsible region.
* @public
*/
MaterialExtCollapsible.prototype.getControlElement = function () {
return this.collapsible.controlElement;
};
MaterialExtCollapsible.prototype['getControlElement'] = MaterialExtCollapsible.prototype.getControlElement;
/**
* Get region elements controlled by this collapsible
* @returns {Array<HTMLElement>} the collapsible region elements
* @public
*/
MaterialExtCollapsible.prototype.getRegionElements = function () {
return this.collapsible.regionElements;
};
MaterialExtCollapsible.prototype['getRegionElements'] = MaterialExtCollapsible.prototype.getRegionElements;
/**
* Add region elements.
* @param {Array<HTMLElement>} elements The element that will be upgraded.
* @return {void}
* @public
*/
MaterialExtCollapsible.prototype.addRegionElements = function () {
var _this3 = this;
for (var _len = arguments.length, elements = Array(_len), _key = 0; _key < _len; _key++) {
elements[_key] = arguments[_key];
}
elements.forEach(function (element) {
return _this3.collapsible.addRegionElement(element);
});
};
MaterialExtCollapsible.prototype['addRegionElements'] = MaterialExtCollapsible.prototype.addRegionElements;
/**
* Remove collapsible region(s) from component.
* Note: This operation does not delete the element from the DOM tree.
* @param {Array<HTMLElement>} elements The element that will be upgraded.
* @public
*/
MaterialExtCollapsible.prototype.removeRegionElements = function () {
var _this4 = this;
for (var _len2 = arguments.length, elements = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
elements[_key2] = arguments[_key2];
}
elements.forEach(function (element) {
return _this4.collapsible.removeRegionElement(element);
});
};
MaterialExtCollapsible.prototype['removeRegionElements'] = MaterialExtCollapsible.prototype.removeRegionElements;
/**
* Expand collapsible region(s)
* @return {void}
* @public
*/
MaterialExtCollapsible.prototype.expand = function () {
this.collapsible.expand();
};
MaterialExtCollapsible.prototype['expand'] = MaterialExtCollapsible.prototype.expand;
/**
* Collapse collapsible region(s)
* @return {void}
* @public
*/
MaterialExtCollapsible.prototype.collapse = function () {
this.collapsible.collapse();
};
MaterialExtCollapsible.prototype['collapse'] = MaterialExtCollapsible.prototype.collapse;
/**
* Toggle collapsible region(s)
* @return {void}
* @public
*/
MaterialExtCollapsible.prototype.toggle = function () {
this.collapsible.toggle();
};
MaterialExtCollapsible.prototype['toggle'] = MaterialExtCollapsible.prototype.toggle;
/**
* Check whether component has aria-expanded state true
* @return {Boolean} true if aria-expanded="true", otherwise false
*/
MaterialExtCollapsible.prototype.isExpanded = function () {
return this.collapsible.isExpanded;
};
MaterialExtCollapsible.prototype['isExpanded'] = MaterialExtCollapsible.prototype.isExpanded;
/**
* Check whether component has aria-disabled state set to true
* @return {Boolean} true if aria-disabled="true", otherwise false
*/
MaterialExtCollapsible.prototype.isDisabled = function () {
return this.collapsible.isDisabled;
};
MaterialExtCollapsible.prototype['isDisabled'] = MaterialExtCollapsible.prototype.isDisabled;
/**
* Disables toggling of collapsible region(s)
* @return {void}
* @public
*/
MaterialExtCollapsible.prototype.disableToggle = function () {
this.collapsible.disableToggle();
};
MaterialExtCollapsible.prototype['disableToggle'] = MaterialExtCollapsible.prototype.disableToggle;
/**
* Enables toggling of collapsible region(s)
* @return {void}
* @public
*/
MaterialExtCollapsible.prototype.enableToggle = function () {
this.collapsible.enableToggle();
};
MaterialExtCollapsible.prototype['enableToggle'] = MaterialExtCollapsible.prototype.enableToggle;
// The component registers itself. It can assume componentHandler is available
// in the global scope.
/* eslint no-undef: 0 */
componentHandler.register({
constructor: MaterialExtCollapsible,
classAsString: 'MaterialExtCollapsible',
cssClass: JS_COLLAPSIBLE,
widget: true
});
})();