diff --git a/src/button/button.js b/src/button/button.js index 090b8bd2..11906b71 100644 --- a/src/button/button.js +++ b/src/button/button.js @@ -96,17 +96,31 @@ MaterialButton.prototype.init = function() { if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) { var rippleContainer = document.createElement('span'); rippleContainer.classList.add(this.CssClasses_.RIPPLE_CONTAINER); - var ripple = document.createElement('span'); - ripple.classList.add(this.CssClasses_.RIPPLE); - rippleContainer.appendChild(ripple); - ripple.addEventListener('mouseup', this.blurHandler.bind(this)); + 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.element_.addEventListener('mouseup', this.blurHandler.bind(this)); - this.element_.addEventListener('mouseleave', this.blurHandler.bind(this)); + this.boundButtonBlurHandler = this.blurHandler.bind(this); + this.element_.addEventListener('mouseup', this.boundButtonBlurHandler); + this.element_.addEventListener('mouseleave', this.boundButtonBlurHandler); } }; +/** +* Downgrade the element. +*/ +MaterialButton.prototype.mdlDowngrade_ = function() { + 'use strict'; + if (this.rippleElement_) { + this.rippleElement_.removeEventListener('mouseup', this.boundRippleBlurHandler); + } + this.element_.removeEventListener('mouseup', this.boundButtonBlurHandler); + this.element_.removeEventListener('mouseleave', this.boundButtonBlurHandler); +}; + // The component registers itself. It can assume componentHandler is available // in the global scope. componentHandler.register({ diff --git a/src/checkbox/checkbox.js b/src/checkbox/checkbox.js index a4ad5f1f..c9869c7c 100644 --- a/src/checkbox/checkbox.js +++ b/src/checkbox/checkbox.js @@ -211,32 +211,49 @@ MaterialCheckbox.prototype.init = function() { this.element_.appendChild(tickContainer); this.element_.appendChild(boxOutline); - var rippleContainer; if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) { this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS); - rippleContainer = document.createElement('span'); - rippleContainer.classList.add(this.CssClasses_.RIPPLE_CONTAINER); - rippleContainer.classList.add(this.CssClasses_.RIPPLE_EFFECT); - rippleContainer.classList.add(this.CssClasses_.RIPPLE_CENTER); - rippleContainer.addEventListener('mouseup', this.onMouseUp_.bind(this)); + 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); - rippleContainer.appendChild(ripple); - this.element_.appendChild(rippleContainer); + this.rippleContainerElement_.appendChild(ripple); + this.element_.appendChild(this.rippleContainerElement_); } - - this.inputElement_.addEventListener('change', this.onChange_.bind(this)); - this.inputElement_.addEventListener('focus', this.onFocus_.bind(this)); - this.inputElement_.addEventListener('blur', this.onBlur_.bind(this)); - this.element_.addEventListener('mouseup', this.onMouseUp_.bind(this)); + 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); } }; +/* +* Downgrade the component. +*/ +MaterialCheckbox.prototype.mdlDowngrade_ = function() { + 'use strict'; + if (this.rippleContainerElement_) { + this.rippleContainerElement_.removeEventListener('mouseup', this.boundRippleMouseUp); + } + this.inputElement_.removeEventListener('change', this.boundInputOnChange); + this.inputElement_.removeEventListener('focus', this.boundInputOnFocus); + this.inputElement_.removeEventListener('blur', this.boundInputOnBlur); + this.element_.removeEventListener('mouseup', this.boundElementMouseUp); +}; + // The component registers itself. It can assume componentHandler is available // in the global scope. componentHandler.register({ diff --git a/src/icon-toggle/icon-toggle.js b/src/icon-toggle/icon-toggle.js index b52d0b02..fd52425c 100644 --- a/src/icon-toggle/icon-toggle.js +++ b/src/icon-toggle/icon-toggle.js @@ -193,32 +193,50 @@ MaterialIconToggle.prototype.init = function() { this.inputElement_ = this.element_.querySelector('.' + this.CssClasses_.INPUT); - var rippleContainer; if (this.element_.classList.contains(this.CssClasses_.JS_RIPPLE_EFFECT)) { this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS); - rippleContainer = document.createElement('span'); - rippleContainer.classList.add(this.CssClasses_.RIPPLE_CONTAINER); - rippleContainer.classList.add(this.CssClasses_.JS_RIPPLE_EFFECT); - rippleContainer.classList.add(this.CssClasses_.RIPPLE_CENTER); - rippleContainer.addEventListener('mouseup', this.onMouseUp_.bind(this)); + 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); - rippleContainer.appendChild(ripple); - this.element_.appendChild(rippleContainer); + this.rippleContainerElement_.appendChild(ripple); + this.element_.appendChild(this.rippleContainerElement_); } - this.inputElement_.addEventListener('change', this.onChange_.bind(this)); - this.inputElement_.addEventListener('focus', this.onFocus_.bind(this)); - this.inputElement_.addEventListener('blur', this.onBlur_.bind(this)); - this.element_.addEventListener('mouseup', this.onMouseUp_.bind(this)); + 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'); } }; +/* +* Downgrade the component +*/ +MaterialIconToggle.prototype.mdlDowngrade_ = function() { + 'use strict'; + if (this.rippleContainerElement_) { + this.rippleContainerElement_.removeEventListener('mouseup', this.boundRippleMouseUp); + } + this.inputElement_.removeEventListener('change', this.boundInputOnChange); + this.inputElement_.removeEventListener('focus', this.boundInputOnFocus); + this.inputElement_.removeEventListener('blur', this.boundInputOnBlur); + this.element_.removeEventListener('mouseup', this.boundElementOnMouseUp); +}; + // The component registers itself. It can assume componentHandler is available // in the global scope. componentHandler.register({ diff --git a/src/mdlComponentHandler.js b/src/mdlComponentHandler.js index 8f009fe8..61465d57 100644 --- a/src/mdlComponentHandler.js +++ b/src/mdlComponentHandler.js @@ -20,12 +20,14 @@ * https://github.com/jasonmayes/mdl-component-design-pattern * @author Jason Mayes. */ - /* exported componentHandler */ +/* exported componentHandler */ var componentHandler = (function() { 'use strict'; var registeredComponents_ = []; var createdComponents_ = []; + var downgradeMethod_ = 'mdlDowngrade_'; + var componentConfigProperty_ = 'mdlComponentConfigInternal_'; /** * Searches registered components for a class we are interested in using. @@ -47,7 +49,6 @@ var componentHandler = (function() { return false; } - /** * Searches existing DOM for elements of our component type and upgrades them * if they have not already been upgraded. @@ -77,7 +78,6 @@ var componentHandler = (function() { } } - /** * Upgrades a specific element rather than all in the DOM. * @param {HTMLElement} element The element we wish to upgrade. @@ -98,6 +98,7 @@ var componentHandler = (function() { if (registeredClass) { // new var instance = new registeredClass.classConstructor(element); + instance[componentConfigProperty_] = registeredClass; createdComponents_.push(instance); // Call any callbacks the user has registered with this component type. registeredClass.callbacks.forEach(function (callback) { @@ -109,9 +110,7 @@ var componentHandler = (function() { element.widget = instance; } } else { - // If component creator forgot to register, try and see if - // it is in global scope. - createdComponents_.push(new window[jsClass](element)); + throw 'Unable to find a registered component for the given class.'; } var ev = document.createEvent('Events'); @@ -120,7 +119,6 @@ var componentHandler = (function() { } } - /** * Registers a class for future use and attempts to upgrade existing DOM. * @param {object} config An object containing: @@ -135,6 +133,19 @@ var componentHandler = (function() { 'callbacks': [] }; + registeredComponents_.forEach(function(item) { + if (item.cssClass === newConfig.cssClass) { + throw 'The provided cssClass has already been registered.'; + } + if (item.className === newConfig.className) { + throw 'The provided className has already been registered'; + } + }); + + if (config.constructor.prototype.hasOwnProperty(componentConfigProperty_)) { + throw 'MDL component classes must not have ' + componentConfigProperty_ + ' defined as a property.'; + } + var found = findRegisteredClass_(config.classAsString, newConfig); if (!found) { @@ -142,7 +153,6 @@ var componentHandler = (function() { } } - /** * Allows user to be alerted to any upgrades that are performed for a given * component type @@ -158,7 +168,6 @@ var componentHandler = (function() { } } - /** * Upgrades all registered components found in the current DOM. This is * automatically called on window load. @@ -169,6 +178,66 @@ var componentHandler = (function() { } } + /** + * Finds a created component by a given DOM node. + * + * @param node + * @returns {*} + */ + function findCreatedComponentByNodeInternal(node) { + for (var n = 0; n < createdComponents_.length; n++) { + var component = createdComponents_[n]; + if (component.element_ === node) { + return component; + } + } + } + + /** + * Check the component for the downgrade method. + * Execute if found. + * Remove component from createdComponents list. + * + * @param component + */ + function deconstructComponentInternal(component) { + if (component && + component[componentConfigProperty_] + .classConstructor.prototype + .hasOwnProperty(downgradeMethod_)) { + component[downgradeMethod_](); + var componentIndex = createdComponents_.indexOf(component); + createdComponents_.splice(componentIndex, 1); + var upgrades = component.element_.dataset.upgraded.split(','); + var componentPlace = upgrades.indexOf(component[componentConfigProperty_].classAsString); + upgrades.splice(componentPlace, 1); + component.element_.dataset.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 nodes + */ + function downgradeNodesInternal(nodes) { + var downgradeNode = function(node) { + deconstructComponentInternal(findCreatedComponentByNodeInternal(node)); + }; + 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 'Invalid argument provided to downgrade MDL nodes.'; + } + } + // Now return the functions that should be made public with their publicly // facing names... return { @@ -176,7 +245,8 @@ var componentHandler = (function() { upgradeElement: upgradeElementInternal, upgradeAllRegistered: upgradeAllRegisteredInternal, registerUpgradedCallback: registerUpgradedCallbackInternal, - register: registerInternal + register: registerInternal, + downgradeElements: downgradeNodesInternal }; })(); diff --git a/src/ripple/ripple.js b/src/ripple/ripple.js index 91479346..1eca3a32 100644 --- a/src/ripple/ripple.js +++ b/src/ripple/ripple.js @@ -142,14 +142,17 @@ MaterialRipple.prototype.init = function() { this.rippleElement_.style.height = this.rippleSize_ + 'px'; } - this.element_.addEventListener('mousedown', this.downHandler_.bind(this)); + this.boundDownHandler = this.downHandler_.bind(this); + this.element_.addEventListener('mousedown', + this.boundDownHandler); this.element_.addEventListener('touchstart', - this.downHandler_.bind(this)); + this.boundDownHandler); - this.element_.addEventListener('mouseup', this.upHandler_.bind(this)); - this.element_.addEventListener('mouseleave', this.upHandler_.bind(this)); - this.element_.addEventListener('touchend', this.upHandler_.bind(this)); - this.element_.addEventListener('blur', this.upHandler_.bind(this)); + this.boundUpHandler = this.upHandler_.bind(this); + this.element_.addEventListener('mouseup', this.boundUpHandler); + this.element_.addEventListener('mouseleave', this.boundUpHandler); + this.element_.addEventListener('touchend', this.boundUpHandler); + this.element_.addEventListener('blur', this.boundUpHandler); this.getFrameCount = function() { return this.frameCount_; @@ -212,6 +215,22 @@ MaterialRipple.prototype.init = function() { } }; +/* +* Downgrade the component +*/ +MaterialRipple.prototype.mdlDowngrade_ = function() { + 'use strict'; + this.element_.removeEventListener('mousedown', + this.boundDownHandler); + this.element_.removeEventListener('touchstart', + this.boundDownHandler); + + this.element_.removeEventListener('mouseup', this.boundUpHandler); + this.element_.removeEventListener('mouseleave', this.boundUpHandler); + this.element_.removeEventListener('touchend', this.boundUpHandler); + this.element_.removeEventListener('blur', this.boundUpHandler); +}; + // The component registers itself. It can assume componentHandler is available // in the global scope. componentHandler.register({ diff --git a/src/slider/slider.js b/src/slider/slider.js index dd182386..b60b2cc1 100644 --- a/src/slider/slider.js +++ b/src/slider/slider.js @@ -102,7 +102,7 @@ MaterialSlider.prototype.onContainerMouseDown_ = function(event) { // If this click is not on the parent element (but rather some child) // ignore. It may still bubble up. - if(event.target !== this.element_.parentElement) { + if (event.target !== this.element_.parentElement) { return; } @@ -216,16 +216,31 @@ MaterialSlider.prototype.init = function() { backgroundFlex.appendChild(this.backgroundUpper_); } - this.element_.addEventListener('input', this.onInput_.bind(this)); - this.element_.addEventListener('change', this.onChange_.bind(this)); - this.element_.addEventListener('mouseup', this.onMouseUp_.bind(this)); - this.element_.parentElement.addEventListener('mousedown', this.onContainerMouseDown_.bind(this)); + this.boundInputHandler = this.onInput_.bind(this); + this.boundChangeHandler = this.onChange_.bind(this); + this.boundMouseUpHandler = this.onMouseUp_.bind(this); + this.boundContainerMouseDownHandler = this.onContainerMouseDown_.bind(this); + this.element_.addEventListener('input', this.boundInputHandler); + this.element_.addEventListener('change', this.boundChangeHandler); + this.element_.addEventListener('mouseup', this.boundMouseUpHandler); + this.element_.parentElement.addEventListener('mousedown', this.boundContainerMouseDownHandler); this.updateValueStyles_(); this.element_.classList.add(this.CssClasses_.IS_UPGRADED); } }; +/* +* Downgrade the component +*/ +MaterialSlider.prototype.mdlDowngrade_ = function() { + 'use strict'; + this.element_.removeEventListener('input', this.boundInputHandler); + this.element_.removeEventListener('change', this.boundChangeHandler); + this.element_.removeEventListener('mouseup', this.boundMouseUpHandler); + this.element_.parentElement.removeEventListener('mousedown', this.boundContainerMouseDownHandler); +}; + // The component registers itself. It can assume componentHandler is available // in the global scope. componentHandler.register({ diff --git a/src/switch/switch.js b/src/switch/switch.js index 1d56e154..a69bc634 100644 --- a/src/switch/switch.js +++ b/src/switch/switch.js @@ -210,35 +210,54 @@ MaterialSwitch.prototype.init = function() { this.element_.appendChild(track); this.element_.appendChild(thumb); - var rippleContainer; + this.boundMouseUpHandler = this.onMouseUp_.bind(this); + if (this.element_.classList.contains( this.CssClasses_.RIPPLE_EFFECT)) { this.element_.classList.add( this.CssClasses_.RIPPLE_IGNORE_EVENTS); - rippleContainer = document.createElement('span'); - rippleContainer.classList.add( + this.rippleContainerElement_ = document.createElement('span'); + this.rippleContainerElement_.classList.add( this.CssClasses_.RIPPLE_CONTAINER); - rippleContainer.classList.add(this.CssClasses_.RIPPLE_EFFECT); - rippleContainer.classList.add(this.CssClasses_.RIPPLE_CENTER); - rippleContainer.addEventListener('mouseup', this.onMouseUp_.bind(this)); + this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_EFFECT); + this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CENTER); + this.rippleContainerElement_.addEventListener('mouseup', this.boundMouseUpHandler); var ripple = document.createElement('span'); ripple.classList.add(this.CssClasses_.RIPPLE); - rippleContainer.appendChild(ripple); - this.element_.appendChild(rippleContainer); + this.rippleContainerElement_.appendChild(ripple); + this.element_.appendChild(this.rippleContainerElement_); } - this.inputElement_.addEventListener('change', this.onChange_.bind(this)); - this.inputElement_.addEventListener('focus', this.onFocus_.bind(this)); - this.inputElement_.addEventListener('blur', this.onBlur_.bind(this)); - this.element_.addEventListener('mouseup', this.onMouseUp_.bind(this)); + this.boundChangeHandler = this.onChange_.bind(this); + this.boundFocusHandler = this.onFocus_.bind(this); + this.boundBlurHandler = this.onBlur_.bind(this); + + this.inputElement_.addEventListener('change', this.boundChangeHandler); + this.inputElement_.addEventListener('focus', this.boundFocusHandler); + this.inputElement_.addEventListener('blur', this.boundBlurHandler); + this.element_.addEventListener('mouseup', this.boundMouseUpHandler); this.updateClasses_(); this.element_.classList.add('is-upgraded'); } }; +/* +* Downgrade the component. +*/ +MaterialSwitch.prototype.mdlDowngrade_ = function() { + 'use strict'; + if (this.rippleContainerElement_) { + this.rippleContainerElement_.removeEventListener('mouseup', this.boundMouseUpHandler); + } + this.inputElement_.removeEventListener('change', this.boundChangeHandler); + this.inputElement_.removeEventListener('focus', this.boundFocusHandler); + this.inputElement_.removeEventListener('blur', this.boundBlurHandler); + this.element_.removeEventListener('mouseup', this.boundMouseUpHandler); +}; + // The component registers itself. It can assume componentHandler is available // in the global scope. componentHandler.register({ diff --git a/src/textfield/textfield.js b/src/textfield/textfield.js index 09e0e41f..1410f6ff 100644 --- a/src/textfield/textfield.js +++ b/src/textfield/textfield.js @@ -179,14 +179,18 @@ MaterialTextfield.prototype.init = function() { } } - this.input_.addEventListener('input', this.updateClasses_.bind(this)); - this.input_.addEventListener('focus', this.onFocus_.bind(this)); - this.input_.addEventListener('blur', this.onBlur_.bind(this)); + this.boundUpdateClassesHandler = this.updateClasses_.bind(this); + this.boundFocusHandler = this.onFocus_.bind(this); + this.boundBlurHandler = this.onBlur_.bind(this); + this.input_.addEventListener('input', this.boundUpdateClassesHandler); + this.input_.addEventListener('focus', this.boundFocusHandler); + this.input_.addEventListener('blur', this.boundBlurHandler); if (this.maxRows !== this.Constant_.NO_MAX_ROWS) { // TODO: This should handle pasting multi line text. // Currently doesn't. - this.input_.addEventListener('keydown', this.onKeyDown_.bind(this)); + this.boundKeyDownHandler = this.onKeyDown_.bind(this); + this.input_.addEventListener('keydown', this.boundKeyDownHandler); } this.updateClasses_(); @@ -195,6 +199,19 @@ MaterialTextfield.prototype.init = function() { } }; +/* +* Downgrade the component +*/ +MaterialTextfield.prototype.mdlDowngrade_ = function() { + 'use strict'; + this.input_.removeEventListener('input', this.boundUpdateClassesHandler); + this.input_.removeEventListener('focus', this.boundFocusHandler); + this.input_.removeEventListener('blur', this.boundBlurHandler); + if (this.boundKeyDownHandler) { + this.input_.removeEventListener('keydown', this.boundKeyDownHandler); + } +}; + // The component registers itself. It can assume componentHandler is available // in the global scope. componentHandler.register({ diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 5744a127..a3b353fb 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -49,7 +49,6 @@ MaterialTooltip.prototype.CssClasses_ = { IS_ACTIVE: 'is-active' }; - /** * Handle mouseenter for tooltip. * @param {Event} event The event that fired. @@ -66,7 +65,6 @@ MaterialTooltip.prototype.handleMouseEnter_ = function(event) { this.element_.classList.add(this.CssClasses_.IS_ACTIVE); }; - /** * Handle mouseleave for tooltip. * @param {Event} event The event that fired. @@ -79,7 +77,6 @@ MaterialTooltip.prototype.handleMouseLeave_ = function(event) { this.element_.classList.remove(this.CssClasses_.IS_ACTIVE); }; - /** * Initialize element. */ @@ -88,20 +85,31 @@ MaterialTooltip.prototype.init = function() { if (this.element_) { var forElId = this.element_.getAttribute('for'); - var forEl = null; if (forElId) { - forEl = document.getElementById(forElId); + this.forElement_ = document.getElementById(forElId); } - if (forEl) { - forEl.addEventListener('mouseenter', this.handleMouseEnter_.bind(this), + if (this.forElement_) { + this.boundMouseEnterHandler = this.handleMouseEnter_.bind(this); + this.boundMouseLeaveHandler = this.handleMouseLeave_.bind(this); + this.forElement_.addEventListener('mouseenter', this.boundMouseEnterHandler, false); - forEl.addEventListener('mouseleave', this.handleMouseLeave_.bind(this)); + this.forElement_.addEventListener('mouseleave', this.boundMouseLeaveHandler); } } }; +/* +* Downgrade the component +*/ +MaterialTooltip.prototype.mdlDowngrade_ = function() { + 'use strict'; + if (this.forElement_) { + this.forElement_.removeEventListener('mouseenter', this.boundMouseEnterHandler, false); + this.forElement_.removeEventListener('mouseleave', this.boundMouseLeaveHandler); + } +}; // The component registers itself. It can assume componentHandler is available // in the global scope.