UNPKG

react-mdl

Version:

React Components wrapper for Material Design Lite UI

1,394 lines (1,356 loc) 144 kB
;(function() { "use strict"; /** * @license * Copyright 2015 Google Inc. 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. */ /** * A component handler interface using the revealing module design pattern. * More details on this design pattern here: * https://github.com/jasonmayes/mdl-component-design-pattern * * @author Jason Mayes. */ /* exported componentHandler */ // Pre-defining the componentHandler interface, for closure documentation and // static verification. var componentHandler = { /** * Searches existing DOM for elements of our component type and upgrades them * if they have not already been upgraded. * * @param {string=} optJsClass the programatic name of the element class we * need to create a new instance of. * @param {string=} optCssClass the name of the CSS class elements of this * type will have. */ upgradeDom: function(optJsClass, optCssClass) {}, /** * Upgrades a specific element rather than all in the DOM. * * @param {!Element} element The element we wish to upgrade. * @param {string=} optJsClass Optional name of the class we want to upgrade * the element to. */ upgradeElement: function(element, optJsClass) {}, /** * Upgrades a specific list of elements rather than all in the DOM. * * @param {!Element|!Array<!Element>|!NodeList|!HTMLCollection} elements * The elements we wish to upgrade. */ upgradeElements: function(elements) {}, /** * Upgrades all registered components found in the current DOM. This is * automatically called on window load. */ upgradeAllRegistered: function() {}, /** * Allows user to be alerted to any upgrades that are performed for a given * component type * * @param {string} jsClass The class name of the MDL component we wish * to hook into for any upgrades performed. * @param {function(!HTMLElement)} callback The function to call upon an * upgrade. This function should expect 1 parameter - the HTMLElement which * got upgraded. */ registerUpgradedCallback: function(jsClass, callback) {}, /** * Registers a class for future use and attempts to upgrade existing DOM. * * @param {componentHandler.ComponentConfigPublic} config the registration configuration */ register: function(config) {}, /** * Downgrade either a given node, an array of nodes, or a NodeList. * * @param {!Node|!Array<!Node>|!NodeList} nodes */ downgradeElements: function(nodes) {} }; componentHandler = (function() { 'use strict'; /** @type {!Array<componentHandler.ComponentConfig>} */ var registeredComponents_ = []; /** @type {!Array<componentHandler.Component>} */ var createdComponents_ = []; var componentConfigProperty_ = 'mdlComponentConfigInternal_'; /** * Searches registered components for a class we are interested in using. * Optionally replaces a match with passed object if specified. * * @param {string} name The name of a class we want to use. * @param {componentHandler.ComponentConfig=} optReplace Optional object to replace match with. * @return {!Object|boolean} * @private */ function findRegisteredClass_(name, optReplace) { for (var i = 0; i < registeredComponents_.length; i++) { if (registeredComponents_[i].className === name) { if (typeof optReplace !== 'undefined') { registeredComponents_[i] = optReplace; } return registeredComponents_[i]; } } return false; } /** * Returns an array of the classNames of the upgraded classes on the element. * * @param {!Element} element The element to fetch data from. * @return {!Array<string>} * @private */ function getUpgradedListOfElement_(element) { var dataUpgraded = element.getAttribute('data-upgraded'); // Use `['']` as default value to conform the `,name,name...` style. return dataUpgraded === null ? [''] : dataUpgraded.split(','); } /** * Returns true if the given element has already been upgraded for the given * class. * * @param {!Element} element The element we want to check. * @param {string} jsClass The class to check for. * @returns {boolean} * @private */ function isElementUpgraded_(element, jsClass) { var upgradedList = getUpgradedListOfElement_(element); return upgradedList.indexOf(jsClass) !== -1; } /** * Searches existing DOM for elements of our component type and upgrades them * if they have not already been upgraded. * * @param {string=} optJsClass the programatic name of the element class we * need to create a new instance of. * @param {string=} optCssClass the name of the CSS class elements of this * type will have. */ function upgradeDomInternal(optJsClass, optCssClass) { if (typeof optJsClass === 'undefined' && typeof optCssClass === 'undefined') { for (var i = 0; i < registeredComponents_.length; i++) { upgradeDomInternal(registeredComponents_[i].className, registeredComponents_[i].cssClass); } } else { var jsClass = /** @type {string} */ (optJsClass); if (typeof optCssClass === 'undefined') { var registeredClass = findRegisteredClass_(jsClass); if (registeredClass) { optCssClass = registeredClass.cssClass; } } var elements = document.querySelectorAll('.' + optCssClass); for (var n = 0; n < elements.length; n++) { upgradeElementInternal(elements[n], jsClass); } } } /** * Upgrades a specific element rather than all in the DOM. * * @param {!Element} element The element we wish to upgrade. * @param {string=} optJsClass Optional name of the class we want to upgrade * the element to. */ function upgradeElementInternal(element, optJsClass) { // Verify argument type. if (!(typeof element === 'object' && element instanceof Element)) { throw new Error('Invalid argument provided to upgrade MDL element.'); } var upgradedList = getUpgradedListOfElement_(element); var classesToUpgrade = []; // If jsClass is not provided scan the registered components to find the // ones matching the element's CSS classList. if (!optJsClass) { var classList = element.classList; registeredComponents_.forEach(function(component) { // Match CSS & Not to be upgraded & Not upgraded. if (classList.contains(component.cssClass) && classesToUpgrade.indexOf(component) === -1 && !isElementUpgraded_(element, component.className)) { classesToUpgrade.push(component); } }); } else if (!isElementUpgraded_(element, optJsClass)) { classesToUpgrade.push(findRegisteredClass_(optJsClass)); } // Upgrade the element for each classes. for (var i = 0, n = classesToUpgrade.length, registeredClass; i < n; i++) { registeredClass = classesToUpgrade[i]; if (registeredClass) { // Mark element as upgraded. upgradedList.push(registeredClass.className); element.setAttribute('data-upgraded', upgradedList.join(',')); var instance = new registeredClass.classConstructor(element); instance[componentConfigProperty_] = registeredClass; createdComponents_.push(instance); // Call any callbacks the user has registered with this component type. for (var j = 0, m = registeredClass.callbacks.length; j < m; j++) { registeredClass.callbacks[j](element); } if (registeredClass.widget) { // Assign per element instance for control over API element[registeredClass.className] = instance; } } else { throw new Error( 'Unable to find a registered component for the given class.'); } var ev = document.createEvent('Events'); ev.initEvent('mdl-componentupgraded', true, true); element.dispatchEvent(ev); } } /** * Upgrades a specific list of elements rather than all in the DOM. * * @param {!Element|!Array<!Element>|!NodeList|!HTMLCollection} elements * The elements we wish to upgrade. */ function upgradeElementsInternal(elements) { if (!Array.isArray(elements)) { if (typeof elements.item === 'function') { elements = Array.prototype.slice.call(/** @type {Array} */ (elements)); } else { elements = [elements]; } } for (var i = 0, n = elements.length, element; i < n; i++) { element = elements[i]; if (element instanceof HTMLElement) { upgradeElementInternal(element); if (element.children.length > 0) { upgradeElementsInternal(element.children); } } } } /** * Registers a class for future use and attempts to upgrade existing DOM. * * @param {componentHandler.ComponentConfigPublic} config */ function registerInternal(config) { // In order to support both Closure-compiled and uncompiled code accessing // this method, we need to allow for both the dot and array syntax for // property access. You'll therefore see the `foo.bar || foo['bar']` // pattern repeated across this method. var widgetMissing = (typeof config.widget === 'undefined' && typeof config['widget'] === 'undefined'); var widget = true; if (!widgetMissing) { widget = config.widget || config['widget']; } var newConfig = /** @type {componentHandler.ComponentConfig} */ ({ classConstructor: config.constructor || config['constructor'], className: config.classAsString || config['classAsString'], cssClass: config.cssClass || config['cssClass'], widget: widget, callbacks: [] }); registeredComponents_.forEach(function(item) { if (item.cssClass === newConfig.cssClass) { throw new Error('The provided cssClass has already been registered: ' + item.cssClass); } if (item.className === newConfig.className) { throw new Error('The provided className has already been registered'); } }); if (config.constructor.prototype .hasOwnProperty(componentConfigProperty_)) { throw new Error( 'MDL component classes must not have ' + componentConfigProperty_ + ' defined as a property.'); } var found = findRegisteredClass_(config.classAsString, newConfig); if (!found) { registeredComponents_.push(newConfig); } } /** * Allows user to be alerted to any upgrades that are performed for a given * component type * * @param {string} jsClass The class name of the MDL component we wish * to hook into for any upgrades performed. * @param {function(!HTMLElement)} callback The function to call upon an * upgrade. This function should expect 1 parameter - the HTMLElement which * got upgraded. */ function registerUpgradedCallbackInternal(jsClass, callback) { var regClass = findRegisteredClass_(jsClass); if (regClass) { regClass.callbacks.push(callback); } } /** * Upgrades all registered components found in the current DOM. This is * automatically called on window load. */ function upgradeAllRegisteredInternal() { for (var n = 0; n < registeredComponents_.length; n++) { upgradeDomInternal(registeredComponents_[n].className); } } /** * Check the component for the downgrade method. * Execute if found. * Remove component from createdComponents list. * * @param {?componentHandler.Component} component */ function deconstructComponentInternal(component) { var componentIndex = createdComponents_.indexOf(component); createdComponents_.splice(componentIndex, 1); var upgrades = component.element_.getAttribute('data-upgraded').split(','); var componentPlace = upgrades.indexOf(component[componentConfigProperty_].classAsString); upgrades.splice(componentPlace, 1); component.element_.setAttribute('data-upgraded', upgrades.join(',')); var ev = document.createEvent('Events'); ev.initEvent('mdl-componentdowngraded', true, true); component.element_.dispatchEvent(ev); } /** * Downgrade either a given node, an array of nodes, or a NodeList. * * @param {!Node|!Array<!Node>|!NodeList} nodes */ function downgradeNodesInternal(nodes) { /** * Auxiliary function to downgrade a single node. * @param {!Node} node the node to be downgraded */ var downgradeNode = function(node) { createdComponents_.filter(function(item) { return item.element_ === node; }).forEach(deconstructComponentInternal); }; if (nodes instanceof Array || nodes instanceof NodeList) { for (var n = 0; n < nodes.length; n++) { downgradeNode(nodes[n]); } } else if (nodes instanceof Node) { downgradeNode(nodes); } else { throw new Error('Invalid argument provided to downgrade MDL nodes.'); } } // Now return the functions that should be made public with their publicly // facing names... return { upgradeDom: upgradeDomInternal, upgradeElement: upgradeElementInternal, upgradeElements: upgradeElementsInternal, upgradeAllRegistered: upgradeAllRegisteredInternal, registerUpgradedCallback: registerUpgradedCallbackInternal, register: registerInternal, downgradeElements: downgradeNodesInternal }; })(); /** * Describes the type of a registered component type managed by * componentHandler. Provided for benefit of the Closure compiler. * * @typedef {{ * constructor: Function, * classAsString: string, * cssClass: string, * widget: (string|boolean|undefined) * }} */ componentHandler.ComponentConfigPublic; // jshint ignore:line /** * Describes the type of a registered component type managed by * componentHandler. Provided for benefit of the Closure compiler. * * @typedef {{ * constructor: !Function, * className: string, * cssClass: string, * widget: (string|boolean), * callbacks: !Array<function(!HTMLElement)> * }} */ componentHandler.ComponentConfig; // jshint ignore:line /** * Created component (i.e., upgraded element) type as managed by * componentHandler. Provided for benefit of the Closure compiler. * * @typedef {{ * element_: !HTMLElement, * className: string, * classAsString: string, * cssClass: string, * widget: string * }} */ componentHandler.Component; // jshint ignore:line // Export all symbols, for the benefit of Closure compiler. // No effect on uncompiled code. componentHandler['upgradeDom'] = componentHandler.upgradeDom; componentHandler['upgradeElement'] = componentHandler.upgradeElement; componentHandler['upgradeElements'] = componentHandler.upgradeElements; componentHandler['upgradeAllRegistered'] = componentHandler.upgradeAllRegistered; componentHandler['registerUpgradedCallback'] = componentHandler.registerUpgradedCallback; componentHandler['register'] = componentHandler.register; componentHandler['downgradeElements'] = componentHandler.downgradeElements; window.componentHandler = componentHandler; window['componentHandler'] = componentHandler; window.addEventListener('load', function() { 'use strict'; /** * Performs a "Cutting the mustard" test. If the browser supports the features * tested, adds a mdl-js class to the <html> element. It then upgrades all MDL * components requiring JavaScript. */ if ('classList' in document.createElement('div') && 'querySelector' in document && 'addEventListener' in window && Array.prototype.forEach) { document.documentElement.classList.add('mdl-js'); componentHandler.upgradeAllRegistered(); } else { /** * Dummy function to avoid JS errors. */ componentHandler.upgradeElement = function() {}; /** * Dummy function to avoid JS errors. */ componentHandler.register = function() {}; } }); // Source: https://github.com/darius/requestAnimationFrame/blob/master/requestAnimationFrame.js // Adapted from https://gist.github.com/paulirish/1579671 which derived from // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating // requestAnimationFrame polyfill by Erik Möller. // Fixes from Paul Irish, Tino Zijdel, Andrew Mao, Klemen Slavič, Darius Bacon // MIT license if (!Date.now) { /** * Date.now polyfill. * @return {number} the current Date */ Date.now = function () { return new Date().getTime(); }; Date['now'] = Date.now; } var vendors = [ 'webkit', 'moz' ]; for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) { var vp = vendors[i]; window.requestAnimationFrame = window[vp + 'RequestAnimationFrame']; window.cancelAnimationFrame = window[vp + 'CancelAnimationFrame'] || window[vp + 'CancelRequestAnimationFrame']; window['requestAnimationFrame'] = window.requestAnimationFrame; window['cancelAnimationFrame'] = window.cancelAnimationFrame; } if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) || !window.requestAnimationFrame || !window.cancelAnimationFrame) { var lastTime = 0; /** * requestAnimationFrame polyfill. * @param {!Function} callback the callback function. */ window.requestAnimationFrame = function (callback) { var now = Date.now(); var nextTime = Math.max(lastTime + 16, now); return setTimeout(function () { callback(lastTime = nextTime); }, nextTime - now); }; window.cancelAnimationFrame = clearTimeout; window['requestAnimationFrame'] = window.requestAnimationFrame; window['cancelAnimationFrame'] = window.cancelAnimationFrame; } /** * @license * Copyright 2015 Google Inc. 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. */ /** * Class constructor for Button MDL component. * Implements MDL component design pattern defined at: * https://github.com/jasonmayes/mdl-component-design-pattern * * @param {HTMLElement} element The element that will be upgraded. */ var MaterialButton = function MaterialButton(element) { this.element_ = element; // Initialize instance. this.init(); }; window['MaterialButton'] = MaterialButton; /** * Store constants in one place so they can be updated easily. * * @enum {string | number} * @private */ MaterialButton.prototype.Constant_ = {}; /** * Store strings for class names defined by this component that are used in * JavaScript. This allows us to simply change it in one place should we * decide to modify at a later date. * * @enum {string} * @private */ MaterialButton.prototype.CssClasses_ = { RIPPLE_EFFECT: 'mdl-js-ripple-effect', RIPPLE_CONTAINER: 'mdl-button__ripple-container', RIPPLE: 'mdl-ripple' }; /** * Handle blur of element. * * @param {Event} event The event that fired. * @private */ MaterialButton.prototype.blurHandler_ = function (event) { if (event) { this.element_.blur(); } }; // Public methods. /** * Disable button. * * @public */ MaterialButton.prototype.disable = function () { this.element_.disabled = true; }; MaterialButton.prototype['disable'] = MaterialButton.prototype.disable; /** * Enable button. * * @public */ MaterialButton.prototype.enable = function () { this.element_.disabled = false; }; MaterialButton.prototype['enable'] = MaterialButton.prototype.enable; /** * Initialize element. */ MaterialButton.prototype.init = function () { if (this.element_) { if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) { var rippleContainer = document.createElement('span'); rippleContainer.classList.add(this.CssClasses_.RIPPLE_CONTAINER); this.rippleElement_ = document.createElement('span'); this.rippleElement_.classList.add(this.CssClasses_.RIPPLE); rippleContainer.appendChild(this.rippleElement_); this.boundRippleBlurHandler = this.blurHandler_.bind(this); this.rippleElement_.addEventListener('mouseup', this.boundRippleBlurHandler); this.element_.appendChild(rippleContainer); } this.boundButtonBlurHandler = this.blurHandler_.bind(this); this.element_.addEventListener('mouseup', this.boundButtonBlurHandler); this.element_.addEventListener('mouseleave', this.boundButtonBlurHandler); } }; // The component registers itself. It can assume componentHandler is available // in the global scope. componentHandler.register({ constructor: MaterialButton, classAsString: 'MaterialButton', cssClass: 'mdl-js-button', widget: true }); /** * @license * Copyright 2015 Google Inc. 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. */ /** * Class constructor for Checkbox MDL component. * Implements MDL component design pattern defined at: * https://github.com/jasonmayes/mdl-component-design-pattern * * @constructor * @param {HTMLElement} element The element that will be upgraded. */ var MaterialCheckbox = function MaterialCheckbox(element) { this.element_ = element; // Initialize instance. this.init(); }; window['MaterialCheckbox'] = MaterialCheckbox; /** * Store constants in one place so they can be updated easily. * * @enum {string | number} * @private */ MaterialCheckbox.prototype.Constant_ = { TINY_TIMEOUT: 0.001 }; /** * Store strings for class names defined by this component that are used in * JavaScript. This allows us to simply change it in one place should we * decide to modify at a later date. * * @enum {string} * @private */ MaterialCheckbox.prototype.CssClasses_ = { INPUT: 'mdl-checkbox__input', BOX_OUTLINE: 'mdl-checkbox__box-outline', FOCUS_HELPER: 'mdl-checkbox__focus-helper', TICK_OUTLINE: 'mdl-checkbox__tick-outline', RIPPLE_EFFECT: 'mdl-js-ripple-effect', RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events', RIPPLE_CONTAINER: 'mdl-checkbox__ripple-container', RIPPLE_CENTER: 'mdl-ripple--center', RIPPLE: 'mdl-ripple', IS_FOCUSED: 'is-focused', IS_DISABLED: 'is-disabled', IS_CHECKED: 'is-checked', IS_UPGRADED: 'is-upgraded' }; /** * Handle change of state. * * @param {Event} event The event that fired. * @private */ MaterialCheckbox.prototype.onChange_ = function (event) { this.updateClasses_(); }; /** * Handle focus of element. * * @param {Event} event The event that fired. * @private */ MaterialCheckbox.prototype.onFocus_ = function (event) { this.element_.classList.add(this.CssClasses_.IS_FOCUSED); }; /** * Handle lost focus of element. * * @param {Event} event The event that fired. * @private */ MaterialCheckbox.prototype.onBlur_ = function (event) { this.element_.classList.remove(this.CssClasses_.IS_FOCUSED); }; /** * Handle mouseup. * * @param {Event} event The event that fired. * @private */ MaterialCheckbox.prototype.onMouseUp_ = function (event) { this.blur_(); }; /** * Handle class updates. * * @private */ MaterialCheckbox.prototype.updateClasses_ = function () { this.checkDisabled(); this.checkToggleState(); }; /** * Add blur. * * @private */ MaterialCheckbox.prototype.blur_ = function () { // TODO: figure out why there's a focus event being fired after our blur, // so that we can avoid this hack. window.setTimeout(function () { this.inputElement_.blur(); }.bind(this), this.Constant_.TINY_TIMEOUT); }; // Public methods. /** * Check the inputs toggle state and update display. * * @public */ MaterialCheckbox.prototype.checkToggleState = function () { if (this.inputElement_.checked) { this.element_.classList.add(this.CssClasses_.IS_CHECKED); } else { this.element_.classList.remove(this.CssClasses_.IS_CHECKED); } }; MaterialCheckbox.prototype['checkToggleState'] = MaterialCheckbox.prototype.checkToggleState; /** * Check the inputs disabled state and update display. * * @public */ MaterialCheckbox.prototype.checkDisabled = function () { if (this.inputElement_.disabled) { this.element_.classList.add(this.CssClasses_.IS_DISABLED); } else { this.element_.classList.remove(this.CssClasses_.IS_DISABLED); } }; MaterialCheckbox.prototype['checkDisabled'] = MaterialCheckbox.prototype.checkDisabled; /** * Disable checkbox. * * @public */ MaterialCheckbox.prototype.disable = function () { this.inputElement_.disabled = true; this.updateClasses_(); }; MaterialCheckbox.prototype['disable'] = MaterialCheckbox.prototype.disable; /** * Enable checkbox. * * @public */ MaterialCheckbox.prototype.enable = function () { this.inputElement_.disabled = false; this.updateClasses_(); }; MaterialCheckbox.prototype['enable'] = MaterialCheckbox.prototype.enable; /** * Check checkbox. * * @public */ MaterialCheckbox.prototype.check = function () { this.inputElement_.checked = true; this.updateClasses_(); }; MaterialCheckbox.prototype['check'] = MaterialCheckbox.prototype.check; /** * Uncheck checkbox. * * @public */ MaterialCheckbox.prototype.uncheck = function () { this.inputElement_.checked = false; this.updateClasses_(); }; MaterialCheckbox.prototype['uncheck'] = MaterialCheckbox.prototype.uncheck; /** * Initialize element. */ MaterialCheckbox.prototype.init = function () { if (this.element_) { this.inputElement_ = this.element_.querySelector('.' + this.CssClasses_.INPUT); var boxOutline = document.createElement('span'); boxOutline.classList.add(this.CssClasses_.BOX_OUTLINE); var tickContainer = document.createElement('span'); tickContainer.classList.add(this.CssClasses_.FOCUS_HELPER); var tickOutline = document.createElement('span'); tickOutline.classList.add(this.CssClasses_.TICK_OUTLINE); boxOutline.appendChild(tickOutline); this.element_.appendChild(tickContainer); this.element_.appendChild(boxOutline); if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) { this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS); this.rippleContainerElement_ = document.createElement('span'); this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CONTAINER); this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_EFFECT); this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CENTER); this.boundRippleMouseUp = this.onMouseUp_.bind(this); this.rippleContainerElement_.addEventListener('mouseup', this.boundRippleMouseUp); var ripple = document.createElement('span'); ripple.classList.add(this.CssClasses_.RIPPLE); this.rippleContainerElement_.appendChild(ripple); this.element_.appendChild(this.rippleContainerElement_); } this.boundInputOnChange = this.onChange_.bind(this); this.boundInputOnFocus = this.onFocus_.bind(this); this.boundInputOnBlur = this.onBlur_.bind(this); this.boundElementMouseUp = this.onMouseUp_.bind(this); this.inputElement_.addEventListener('change', this.boundInputOnChange); this.inputElement_.addEventListener('focus', this.boundInputOnFocus); this.inputElement_.addEventListener('blur', this.boundInputOnBlur); this.element_.addEventListener('mouseup', this.boundElementMouseUp); this.updateClasses_(); this.element_.classList.add(this.CssClasses_.IS_UPGRADED); } }; // The component registers itself. It can assume componentHandler is available // in the global scope. componentHandler.register({ constructor: MaterialCheckbox, classAsString: 'MaterialCheckbox', cssClass: 'mdl-js-checkbox', widget: true }); /** * @license * Copyright 2015 Google Inc. 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. */ /** * Class constructor for icon toggle MDL component. * Implements MDL component design pattern defined at: * https://github.com/jasonmayes/mdl-component-design-pattern * * @constructor * @param {HTMLElement} element The element that will be upgraded. */ var MaterialIconToggle = function MaterialIconToggle(element) { this.element_ = element; // Initialize instance. this.init(); }; window['MaterialIconToggle'] = MaterialIconToggle; /** * Store constants in one place so they can be updated easily. * * @enum {string | number} * @private */ MaterialIconToggle.prototype.Constant_ = { TINY_TIMEOUT: 0.001 }; /** * Store strings for class names defined by this component that are used in * JavaScript. This allows us to simply change it in one place should we * decide to modify at a later date. * * @enum {string} * @private */ MaterialIconToggle.prototype.CssClasses_ = { INPUT: 'mdl-icon-toggle__input', JS_RIPPLE_EFFECT: 'mdl-js-ripple-effect', RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events', RIPPLE_CONTAINER: 'mdl-icon-toggle__ripple-container', RIPPLE_CENTER: 'mdl-ripple--center', RIPPLE: 'mdl-ripple', IS_FOCUSED: 'is-focused', IS_DISABLED: 'is-disabled', IS_CHECKED: 'is-checked' }; /** * Handle change of state. * * @param {Event} event The event that fired. * @private */ MaterialIconToggle.prototype.onChange_ = function (event) { this.updateClasses_(); }; /** * Handle focus of element. * * @param {Event} event The event that fired. * @private */ MaterialIconToggle.prototype.onFocus_ = function (event) { this.element_.classList.add(this.CssClasses_.IS_FOCUSED); }; /** * Handle lost focus of element. * * @param {Event} event The event that fired. * @private */ MaterialIconToggle.prototype.onBlur_ = function (event) { this.element_.classList.remove(this.CssClasses_.IS_FOCUSED); }; /** * Handle mouseup. * * @param {Event} event The event that fired. * @private */ MaterialIconToggle.prototype.onMouseUp_ = function (event) { this.blur_(); }; /** * Handle class updates. * * @private */ MaterialIconToggle.prototype.updateClasses_ = function () { this.checkDisabled(); this.checkToggleState(); }; /** * Add blur. * * @private */ MaterialIconToggle.prototype.blur_ = function () { // TODO: figure out why there's a focus event being fired after our blur, // so that we can avoid this hack. window.setTimeout(function () { this.inputElement_.blur(); }.bind(this), this.Constant_.TINY_TIMEOUT); }; // Public methods. /** * Check the inputs toggle state and update display. * * @public */ MaterialIconToggle.prototype.checkToggleState = function () { if (this.inputElement_.checked) { this.element_.classList.add(this.CssClasses_.IS_CHECKED); } else { this.element_.classList.remove(this.CssClasses_.IS_CHECKED); } }; MaterialIconToggle.prototype['checkToggleState'] = MaterialIconToggle.prototype.checkToggleState; /** * Check the inputs disabled state and update display. * * @public */ MaterialIconToggle.prototype.checkDisabled = function () { if (this.inputElement_.disabled) { this.element_.classList.add(this.CssClasses_.IS_DISABLED); } else { this.element_.classList.remove(this.CssClasses_.IS_DISABLED); } }; MaterialIconToggle.prototype['checkDisabled'] = MaterialIconToggle.prototype.checkDisabled; /** * Disable icon toggle. * * @public */ MaterialIconToggle.prototype.disable = function () { this.inputElement_.disabled = true; this.updateClasses_(); }; MaterialIconToggle.prototype['disable'] = MaterialIconToggle.prototype.disable; /** * Enable icon toggle. * * @public */ MaterialIconToggle.prototype.enable = function () { this.inputElement_.disabled = false; this.updateClasses_(); }; MaterialIconToggle.prototype['enable'] = MaterialIconToggle.prototype.enable; /** * Check icon toggle. * * @public */ MaterialIconToggle.prototype.check = function () { this.inputElement_.checked = true; this.updateClasses_(); }; MaterialIconToggle.prototype['check'] = MaterialIconToggle.prototype.check; /** * Uncheck icon toggle. * * @public */ MaterialIconToggle.prototype.uncheck = function () { this.inputElement_.checked = false; this.updateClasses_(); }; MaterialIconToggle.prototype['uncheck'] = MaterialIconToggle.prototype.uncheck; /** * Initialize element. */ MaterialIconToggle.prototype.init = function () { if (this.element_) { this.inputElement_ = this.element_.querySelector('.' + this.CssClasses_.INPUT); if (this.element_.classList.contains(this.CssClasses_.JS_RIPPLE_EFFECT)) { this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS); this.rippleContainerElement_ = document.createElement('span'); this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CONTAINER); this.rippleContainerElement_.classList.add(this.CssClasses_.JS_RIPPLE_EFFECT); this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CENTER); this.boundRippleMouseUp = this.onMouseUp_.bind(this); this.rippleContainerElement_.addEventListener('mouseup', this.boundRippleMouseUp); var ripple = document.createElement('span'); ripple.classList.add(this.CssClasses_.RIPPLE); this.rippleContainerElement_.appendChild(ripple); this.element_.appendChild(this.rippleContainerElement_); } this.boundInputOnChange = this.onChange_.bind(this); this.boundInputOnFocus = this.onFocus_.bind(this); this.boundInputOnBlur = this.onBlur_.bind(this); this.boundElementOnMouseUp = this.onMouseUp_.bind(this); this.inputElement_.addEventListener('change', this.boundInputOnChange); this.inputElement_.addEventListener('focus', this.boundInputOnFocus); this.inputElement_.addEventListener('blur', this.boundInputOnBlur); this.element_.addEventListener('mouseup', this.boundElementOnMouseUp); this.updateClasses_(); this.element_.classList.add('is-upgraded'); } }; // The component registers itself. It can assume componentHandler is available // in the global scope. componentHandler.register({ constructor: MaterialIconToggle, classAsString: 'MaterialIconToggle', cssClass: 'mdl-js-icon-toggle', widget: true }); /** * @license * Copyright 2015 Google Inc. 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. */ /** * Class constructor for dropdown MDL component. * Implements MDL component design pattern defined at: * https://github.com/jasonmayes/mdl-component-design-pattern * * @constructor * @param {HTMLElement} element The element that will be upgraded. */ var MaterialMenu = function MaterialMenu(element) { this.element_ = element; // Initialize instance. this.init(); }; window['MaterialMenu'] = MaterialMenu; /** * Store constants in one place so they can be updated easily. * * @enum {string | number} * @private */ MaterialMenu.prototype.Constant_ = { // Total duration of the menu animation. TRANSITION_DURATION_SECONDS: 0.3, // The fraction of the total duration we want to use for menu item animations. TRANSITION_DURATION_FRACTION: 0.8, // How long the menu stays open after choosing an option (so the user can see // the ripple). CLOSE_TIMEOUT: 150 }; /** * Keycodes, for code readability. * * @enum {number} * @private */ MaterialMenu.prototype.Keycodes_ = { ENTER: 13, ESCAPE: 27, SPACE: 32, UP_ARROW: 38, DOWN_ARROW: 40 }; /** * Store strings for class names defined by this component that are used in * JavaScript. This allows us to simply change it in one place should we * decide to modify at a later date. * * @enum {string} * @private */ MaterialMenu.prototype.CssClasses_ = { CONTAINER: 'mdl-menu__container', OUTLINE: 'mdl-menu__outline', ITEM: 'mdl-menu__item', ITEM_RIPPLE_CONTAINER: 'mdl-menu__item-ripple-container', RIPPLE_EFFECT: 'mdl-js-ripple-effect', RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events', RIPPLE: 'mdl-ripple', // Statuses IS_UPGRADED: 'is-upgraded', IS_VISIBLE: 'is-visible', IS_ANIMATING: 'is-animating', // Alignment options BOTTOM_LEFT: 'mdl-menu--bottom-left', // This is the default. BOTTOM_RIGHT: 'mdl-menu--bottom-right', TOP_LEFT: 'mdl-menu--top-left', TOP_RIGHT: 'mdl-menu--top-right', UNALIGNED: 'mdl-menu--unaligned' }; /** * Initialize element. */ MaterialMenu.prototype.init = function () { if (this.element_) { // Create container for the menu. var container = document.createElement('div'); container.classList.add(this.CssClasses_.CONTAINER); this.element_.parentElement.insertBefore(container, this.element_); this.element_.parentElement.removeChild(this.element_); container.appendChild(this.element_); this.container_ = container; // Create outline for the menu (shadow and background). var outline = document.createElement('div'); outline.classList.add(this.CssClasses_.OUTLINE); this.outline_ = outline; container.insertBefore(outline, this.element_); // Find the "for" element and bind events to it. var forElId = this.element_.getAttribute('for') || this.element_.getAttribute('data-mdl-for'); var forEl = null; if (forElId) { forEl = document.getElementById(forElId); if (forEl) { this.forElement_ = forEl; forEl.addEventListener('click', this.handleForClick_.bind(this)); forEl.addEventListener('keydown', this.handleForKeyboardEvent_.bind(this)); } } var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM); this.boundItemKeydown_ = this.handleItemKeyboardEvent_.bind(this); this.boundItemClick_ = this.handleItemClick_.bind(this); for (var i = 0; i < items.length; i++) { // Add a listener to each menu item. items[i].addEventListener('click', this.boundItemClick_); // Add a tab index to each menu item. items[i].tabIndex = '-1'; // Add a keyboard listener to each menu item. items[i].addEventListener('keydown', this.boundItemKeydown_); } // Add ripple classes to each item, if the user has enabled ripples. if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) { this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS); for (i = 0; i < items.length; i++) { var item = items[i]; var rippleContainer = document.createElement('span'); rippleContainer.classList.add(this.CssClasses_.ITEM_RIPPLE_CONTAINER); var ripple = document.createElement('span'); ripple.classList.add(this.CssClasses_.RIPPLE); rippleContainer.appendChild(ripple); item.appendChild(rippleContainer); item.classList.add(this.CssClasses_.RIPPLE_EFFECT); } } // Copy alignment classes to the container, so the outline can use them. if (this.element_.classList.contains(this.CssClasses_.BOTTOM_LEFT)) { this.outline_.classList.add(this.CssClasses_.BOTTOM_LEFT); } if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) { this.outline_.classList.add(this.CssClasses_.BOTTOM_RIGHT); } if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) { this.outline_.classList.add(this.CssClasses_.TOP_LEFT); } if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) { this.outline_.classList.add(this.CssClasses_.TOP_RIGHT); } if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) { this.outline_.classList.add(this.CssClasses_.UNALIGNED); } container.classList.add(this.CssClasses_.IS_UPGRADED); } }; /** * Handles a click on the "for" element, by positioning the menu and then * toggling it. * * @param {Event} evt The event that fired. * @private */ MaterialMenu.prototype.handleForClick_ = function (evt) { if (this.element_ && this.forElement_) { var rect = this.forElement_.getBoundingClientRect(); var forRect = this.forElement_.parentElement.getBoundingClientRect(); if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) { } else if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) { // Position below the "for" element, aligned to its right. this.container_.style.right = forRect.right - rect.right + 'px'; this.container_.style.top = this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px'; } else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) { // Position above the "for" element, aligned to its left. this.container_.style.left = this.forElement_.offsetLeft + 'px'; this.container_.style.bottom = forRect.bottom - rect.top + 'px'; } else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) { // Position above the "for" element, aligned to its right. this.container_.style.right = forRect.right - rect.right + 'px'; this.container_.style.bottom = forRect.bottom - rect.top + 'px'; } else { // Default: position below the "for" element, aligned to its left. this.container_.style.left = this.forElement_.offsetLeft + 'px'; this.container_.style.top = this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px'; } } this.toggle(evt); }; /** * Handles a keyboard event on the "for" element. * * @param {Event} evt The event that fired. * @private */ MaterialMenu.prototype.handleForKeyboardEvent_ = function (evt) { if (this.element_ && this.container_ && this.forElement_) { var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM + ':not([disabled])'); if (items && items.length > 0 && this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) { if (evt.keyCode === this.Keycodes_.UP_ARROW) { evt.preventDefault(); items[items.length - 1].focus(); } else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) { evt.preventDefault(); items[0].focus(); } } } }; /** * Handles a keyboard event on an item. * * @param {Event} evt The event that fired. * @private */ MaterialMenu.prototype.handleItemKeyboardEvent_ = function (evt) { if (this.element_ && this.container_) { var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM + ':not([disabled])'); if (items && items.length > 0 && this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) { var currentIndex = Array.prototype.slice.call(items).indexOf(evt.target); if (evt.keyCode === this.Keycodes_.UP_ARROW) { evt.preventDefault(); if (currentIndex > 0) { items[currentIndex - 1].focus(); } else { items[items.length - 1].focus(); } } else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) { evt.preventDefault(); if (items.length > currentIndex + 1) { items[currentIndex + 1].focus(); } else { items[0].focus(); } } else if (evt.keyCode === this.Keycodes_.SPACE || evt.keyCode === this.Keycodes_.ENTER) { evt.preventDefault(); // Send mousedown and mouseup to trigger ripple. var e = new MouseEvent('mousedown'); evt.target.dispatchEvent(e); e = new MouseEvent('mouseup'); evt.target.dispatchEvent(e); // Send click. evt.target.click(); } else if (evt.keyCode === this.Keycodes_.ESCAPE) { evt.preventDefault(); this.hide(); } } } }; /** * Handles a click event on an item. * * @param {Event} evt The event that fired. * @private */ MaterialMenu.prototype.handleItemClick_ = function (evt) { if (evt.target.hasAttribute('disabled')) { evt.stopPropagation(); } else { // Wait some time before closing menu, so the user can see the ripple. this.closing_ = true; window.setTimeout(function (evt) { this.hide(); this.closing_ = false; }.bind(this), this.Constant_.CLOSE_TIMEOUT); } }; /** * Calculates the initial clip (for opening the menu) or final clip (for closing * it), and applies it. This allows us to animate from or to the correct point, * that is, the point it's aligned to in the "for" element. * * @param {number} height Height of the clip rectangle * @param {number} width Width of the clip rectangle * @private */ MaterialMenu.prototype.applyClip_ = function (height, width) { if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) { // Do not clip. this.element_.style.clip = ''; } else if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) { // Clip to the top right corner of the menu. this.element_.style.clip = 'rect(0 ' + width + 'px ' + '0 ' + width + 'px)'; } else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) { // Clip to the bottom left corner of the menu. this.element_.style.clip = 'rect(' + height + 'px 0 ' + height + 'px 0)'; } else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) { // Clip to the bottom right corner of the menu. this.element_.style.clip = 'rect(' + height + 'px ' + width + 'px ' + height + 'px ' + width + 'px)'; } else { // Default: do not clip (same as clipping to the top left corner). this.element_.style.clip = ''; } }; /** * Cleanup function to remove animation listeners. * * @param {Event} evt * @private */ MaterialMenu.prototype.removeAnimationEndListener_ = function (evt) { evt.target.classList.remove(MaterialMenu.prototype.CssClasses_.IS_ANIMATING); }; /** * Adds an event listener to clean up after the animation ends. * * @private */ MaterialMenu.prototype.addAnimationEndListener_ = function () { this.element_.addEventListener('transitionend', this.removeAnimationEndListener_)