Project import generated by Copybara.
GitOrigin-RevId: 63746295f1a5ab5a619056791995793d65529e62
diff --git a/node_modules/mdl-ext/src/collapsible/collapsible.js b/node_modules/mdl-ext/src/collapsible/collapsible.js
new file mode 100644
index 0000000..862fffb
--- /dev/null
+++ b/node_modules/mdl-ext/src/collapsible/collapsible.js
@@ -0,0 +1,437 @@
+/**
+ * @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
+ */
+
+import {
+ IS_UPGRADED,
+ VK_SPACE,
+ VK_ENTER,
+} from '../utils/constants';
+
+import { randomString } from '../utils/string-utils';
+import { getParentElements, isFocusable } from '../utils/dom-utils';
+
+const JS_COLLAPSIBLE = 'mdlext-js-collapsible';
+const COLLAPSIBLE_CONTROL_CLASS = 'mdlext-collapsible';
+const COLLAPSIBLE_GROUP_CLASS = 'mdlext-collapsible-group';
+const COLLAPSIBLE_REGION_CLASS = 'mdlext-collapsible-region';
+
+/**
+ * The collapsible component
+ */
+
+class Collapsible {
+ element_ = null;
+ controlElement_ = null;
+
+ /**
+ * @constructor
+ * @param {HTMLElement} element The element that this component is connected to.
+ */
+ constructor(element) {
+ this.element_ = element;
+ this.init();
+ }
+
+ keyDownHandler = event => {
+ if (event.keyCode === VK_ENTER || event.keyCode === VK_SPACE) {
+ event.preventDefault();
+
+ // Trigger click
+ (event.target || this.controlElement).dispatchEvent(
+ new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ view: window
+ })
+ );
+ }
+ };
+
+ clickHandler = event => {
+ if(!this.isDisabled) {
+ if(event.target !== this.controlElement) {
+ // Do not toggle if a focusable element inside the control element triggered the event
+ const p = getParentElements(event.target, this.controlElement);
+ p.push(event.target);
+ if(p.find( el => isFocusable(el))) {
+ return;
+ }
+ }
+ this.toggle();
+ }
+ };
+
+ get element() {
+ return this.element_;
+ }
+
+ get controlElement() {
+ return this.controlElement_;
+ }
+
+ get isDisabled() {
+ return (this.controlElement.hasAttribute('disabled') &&
+ this.controlElement.getAttribute('disabled').toLowerCase() !== 'false') ||
+ (this.controlElement.hasAttribute('aria-disabled') &&
+ this.controlElement.getAttribute('aria-disabled').toLowerCase() !== 'false');
+ }
+
+ get isExpanded() {
+ return this.controlElement.hasAttribute('aria-expanded') &&
+ this.controlElement.getAttribute('aria-expanded').toLowerCase() === 'true';
+ }
+
+ get regionIds() {
+ return this.controlElement.hasAttribute('aria-controls')
+ ? this.controlElement.getAttribute('aria-controls').split(' ')
+ : [];
+ }
+
+ get regionElements() {
+ return this.regionIds
+ .map(id => document.querySelector(`#${id}`))
+ .filter( el => el != null);
+ }
+
+ collapse() {
+ if(!this.isDisabled && this.isExpanded) {
+ if(this.dispatchToggleEvent('collapse')) {
+ this.controlElement.setAttribute('aria-expanded', 'false');
+ const regions = this.regionElements.slice(0);
+ for (let i = regions.length - 1; i >= 0; --i) {
+ regions[i].setAttribute('hidden', '');
+ }
+ }
+ }
+ }
+
+ expand() {
+ if(!this.isDisabled && !this.isExpanded) {
+ if(this.dispatchToggleEvent('expand')) {
+ this.controlElement.setAttribute('aria-expanded', 'true');
+ this.regionElements.forEach(region => region.removeAttribute('hidden'));
+ }
+ }
+ }
+
+ toggle() {
+ if (this.isExpanded) {
+ this.collapse();
+ }
+ else {
+ this.expand();
+ }
+ }
+
+ dispatchToggleEvent(action) {
+ return this.element.dispatchEvent(
+ new CustomEvent('toggle', {
+ bubbles: true,
+ cancelable: true,
+ detail: {
+ action: action
+ }
+ })
+ );
+ }
+
+ disableToggle() {
+ this.controlElement.setAttribute('aria-disabled', true);
+ }
+
+ enableToggle() {
+ this.controlElement.removeAttribute('aria-disabled');
+ }
+
+ addRegionId(regionId) {
+ const ids = this.regionIds;
+ if(!ids.find(id => regionId === id)) {
+ ids.push(regionId);
+ this.controlElement.setAttribute('aria-controls', ids.join(' '));
+ }
+ }
+
+ 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')) {
+ const role = region.classList.contains(COLLAPSIBLE_GROUP_CLASS) ? 'group' : 'region';
+ region.setAttribute('role', role);
+ }
+
+ if(!region.hasAttribute('id')) {
+ region.id = `${region.getAttribute('role')}-${randomString()}`;
+ }
+
+ if(this.isExpanded) {
+ region.removeAttribute('hidden');
+ }
+ else {
+ region.setAttribute('hidden', '');
+ }
+ this.addRegionId(region.id);
+ }
+
+ removeRegionElement(region) {
+ if(region && region.id) {
+ const ids = this.regionIds.filter(id => id === region.id);
+ this.controlElement.setAttribute('aria-controls', ids.join(' '));
+ }
+ }
+
+ removeListeners() {
+ this.controlElement.removeEventListener('keydown', this.keyDownHandler);
+ this.controlElement.removeEventListener('click', this.clickHandler);
+ }
+
+ init() {
+ const initControl = () => {
+ // Find the button element
+ this.controlElement_ = this.element.querySelector(`.${COLLAPSIBLE_CONTROL_CLASS}`) || this.element;
+
+ // Add "aria-expanded" attribute if not present
+ if(!this.controlElement.hasAttribute('aria-expanded')) {
+ this.controlElement.setAttribute('aria-expanded', 'false');
+ }
+
+ // Add role=button if control != <button>
+ if(this.controlElement.nodeName.toLowerCase() !== 'button') {
+ this.controlElement.setAttribute('role', 'button');
+ }
+
+ // Add tabindex
+ if(!isFocusable(this.controlElement) && !this.controlElement.hasAttribute('tabindex')) {
+ this.controlElement.setAttribute('tabindex', '0');
+ }
+ };
+
+ const initRegions = () => {
+ let regions = [];
+ if(!this.controlElement.hasAttribute('aria-controls')) {
+ // Add siblings as collapsible region(s)
+ let r = this.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 = this.regionElements;
+ }
+ regions.forEach(region => this.addRegionElement(region));
+ };
+
+ const addListeners = () => {
+ this.controlElement.addEventListener('keydown', this.keyDownHandler);
+ this.controlElement.addEventListener('click', this.clickHandler);
+ };
+
+ initControl();
+ initRegions();
+ this.removeListeners();
+ addListeners();
+ }
+
+ downgrade() {
+ this.removeListeners();
+ }
+
+}
+
+(function() {
+ 'use strict';
+
+ /**
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+ const 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(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(...elements) {
+ elements.forEach(element => this.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(...elements) {
+ elements.forEach(element => this.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
+ });
+
+})();