Copybara bot | be50d49 | 2023-11-30 00:16:42 +0100 | [diff] [blame] | 1 | /** |
| 2 | * @license |
| 3 | * Copyright 2015 Google Inc. All Rights Reserved. |
| 4 | * |
| 5 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | * you may not use this file except in compliance with the License. |
| 7 | * You may obtain a copy of the License at |
| 8 | * |
| 9 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | * |
| 11 | * Unless required by applicable law or agreed to in writing, software |
| 12 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | * See the License for the specific language governing permissions and |
| 15 | * limitations under the License. |
| 16 | */ |
| 17 | |
| 18 | /** |
| 19 | * A component handler interface using the revealing module design pattern. |
| 20 | * More details on this design pattern here: |
| 21 | * https://github.com/jasonmayes/mdl-component-design-pattern |
| 22 | * |
| 23 | * @author Jason Mayes. |
| 24 | */ |
| 25 | /* exported componentHandler */ |
| 26 | |
| 27 | // Pre-defining the componentHandler interface, for closure documentation and |
| 28 | // static verification. |
| 29 | var componentHandler = { |
| 30 | /** |
| 31 | * Searches existing DOM for elements of our component type and upgrades them |
| 32 | * if they have not already been upgraded. |
| 33 | * |
| 34 | * @param {string=} optJsClass the programatic name of the element class we |
| 35 | * need to create a new instance of. |
| 36 | * @param {string=} optCssClass the name of the CSS class elements of this |
| 37 | * type will have. |
| 38 | */ |
| 39 | upgradeDom: function(optJsClass, optCssClass) {}, |
| 40 | /** |
| 41 | * Upgrades a specific element rather than all in the DOM. |
| 42 | * |
| 43 | * @param {!Element} element The element we wish to upgrade. |
| 44 | * @param {string=} optJsClass Optional name of the class we want to upgrade |
| 45 | * the element to. |
| 46 | */ |
| 47 | upgradeElement: function(element, optJsClass) {}, |
| 48 | /** |
| 49 | * Upgrades a specific list of elements rather than all in the DOM. |
| 50 | * |
| 51 | * @param {!Element|!Array<!Element>|!NodeList|!HTMLCollection} elements |
| 52 | * The elements we wish to upgrade. |
| 53 | */ |
| 54 | upgradeElements: function(elements) {}, |
| 55 | /** |
| 56 | * Upgrades all registered components found in the current DOM. This is |
| 57 | * automatically called on window load. |
| 58 | */ |
| 59 | upgradeAllRegistered: function() {}, |
| 60 | /** |
| 61 | * Allows user to be alerted to any upgrades that are performed for a given |
| 62 | * component type |
| 63 | * |
| 64 | * @param {string} jsClass The class name of the MDL component we wish |
| 65 | * to hook into for any upgrades performed. |
| 66 | * @param {function(!HTMLElement)} callback The function to call upon an |
| 67 | * upgrade. This function should expect 1 parameter - the HTMLElement which |
| 68 | * got upgraded. |
| 69 | */ |
| 70 | registerUpgradedCallback: function(jsClass, callback) {}, |
| 71 | /** |
| 72 | * Registers a class for future use and attempts to upgrade existing DOM. |
| 73 | * |
| 74 | * @param {componentHandler.ComponentConfigPublic} config the registration configuration |
| 75 | */ |
| 76 | register: function(config) {}, |
| 77 | /** |
| 78 | * Downgrade either a given node, an array of nodes, or a NodeList. |
| 79 | * |
| 80 | * @param {!Node|!Array<!Node>|!NodeList} nodes |
| 81 | */ |
| 82 | downgradeElements: function(nodes) {} |
| 83 | }; |
| 84 | |
| 85 | componentHandler = (function() { |
| 86 | 'use strict'; |
| 87 | |
| 88 | /** @type {!Array<componentHandler.ComponentConfig>} */ |
| 89 | var registeredComponents_ = []; |
| 90 | |
| 91 | /** @type {!Array<componentHandler.Component>} */ |
| 92 | var createdComponents_ = []; |
| 93 | |
| 94 | var componentConfigProperty_ = 'mdlComponentConfigInternal_'; |
| 95 | |
| 96 | /** |
| 97 | * Searches registered components for a class we are interested in using. |
| 98 | * Optionally replaces a match with passed object if specified. |
| 99 | * |
| 100 | * @param {string} name The name of a class we want to use. |
| 101 | * @param {componentHandler.ComponentConfig=} optReplace Optional object to replace match with. |
| 102 | * @return {!Object|boolean} |
| 103 | * @private |
| 104 | */ |
| 105 | function findRegisteredClass_(name, optReplace) { |
| 106 | for (var i = 0; i < registeredComponents_.length; i++) { |
| 107 | if (registeredComponents_[i].className === name) { |
| 108 | if (typeof optReplace !== 'undefined') { |
| 109 | registeredComponents_[i] = optReplace; |
| 110 | } |
| 111 | return registeredComponents_[i]; |
| 112 | } |
| 113 | } |
| 114 | return false; |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * Returns an array of the classNames of the upgraded classes on the element. |
| 119 | * |
| 120 | * @param {!Element} element The element to fetch data from. |
| 121 | * @return {!Array<string>} |
| 122 | * @private |
| 123 | */ |
| 124 | function getUpgradedListOfElement_(element) { |
| 125 | var dataUpgraded = element.getAttribute('data-upgraded'); |
| 126 | // Use `['']` as default value to conform the `,name,name...` style. |
| 127 | return dataUpgraded === null ? [''] : dataUpgraded.split(','); |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * Returns true if the given element has already been upgraded for the given |
| 132 | * class. |
| 133 | * |
| 134 | * @param {!Element} element The element we want to check. |
| 135 | * @param {string} jsClass The class to check for. |
| 136 | * @returns {boolean} |
| 137 | * @private |
| 138 | */ |
| 139 | function isElementUpgraded_(element, jsClass) { |
| 140 | var upgradedList = getUpgradedListOfElement_(element); |
| 141 | return upgradedList.indexOf(jsClass) !== -1; |
| 142 | } |
| 143 | |
| 144 | /** |
| 145 | * Create an event object. |
| 146 | * |
| 147 | * @param {string} eventType The type name of the event. |
| 148 | * @param {boolean} bubbles Whether the event should bubble up the DOM. |
| 149 | * @param {boolean} cancelable Whether the event can be canceled. |
| 150 | * @returns {!Event} |
| 151 | */ |
| 152 | function createEvent_(eventType, bubbles, cancelable) { |
| 153 | if ('CustomEvent' in window && typeof window.CustomEvent === 'function') { |
| 154 | return new CustomEvent(eventType, { |
| 155 | bubbles: bubbles, |
| 156 | cancelable: cancelable |
| 157 | }); |
| 158 | } else { |
| 159 | var ev = document.createEvent('Events'); |
| 160 | ev.initEvent(eventType, bubbles, cancelable); |
| 161 | return ev; |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * Searches existing DOM for elements of our component type and upgrades them |
| 167 | * if they have not already been upgraded. |
| 168 | * |
| 169 | * @param {string=} optJsClass the programatic name of the element class we |
| 170 | * need to create a new instance of. |
| 171 | * @param {string=} optCssClass the name of the CSS class elements of this |
| 172 | * type will have. |
| 173 | */ |
| 174 | function upgradeDomInternal(optJsClass, optCssClass) { |
| 175 | if (typeof optJsClass === 'undefined' && |
| 176 | typeof optCssClass === 'undefined') { |
| 177 | for (var i = 0; i < registeredComponents_.length; i++) { |
| 178 | upgradeDomInternal(registeredComponents_[i].className, |
| 179 | registeredComponents_[i].cssClass); |
| 180 | } |
| 181 | } else { |
| 182 | var jsClass = /** @type {string} */ (optJsClass); |
| 183 | if (typeof optCssClass === 'undefined') { |
| 184 | var registeredClass = findRegisteredClass_(jsClass); |
| 185 | if (registeredClass) { |
| 186 | optCssClass = registeredClass.cssClass; |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | var elements = document.querySelectorAll('.' + optCssClass); |
| 191 | for (var n = 0; n < elements.length; n++) { |
| 192 | upgradeElementInternal(elements[n], jsClass); |
| 193 | } |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | /** |
| 198 | * Upgrades a specific element rather than all in the DOM. |
| 199 | * |
| 200 | * @param {!Element} element The element we wish to upgrade. |
| 201 | * @param {string=} optJsClass Optional name of the class we want to upgrade |
| 202 | * the element to. |
| 203 | */ |
| 204 | function upgradeElementInternal(element, optJsClass) { |
| 205 | // Verify argument type. |
| 206 | if (!(typeof element === 'object' && element instanceof Element)) { |
| 207 | throw new Error('Invalid argument provided to upgrade MDL element.'); |
| 208 | } |
| 209 | // Allow upgrade to be canceled by canceling emitted event. |
| 210 | var upgradingEv = createEvent_('mdl-componentupgrading', true, true); |
| 211 | element.dispatchEvent(upgradingEv); |
| 212 | if (upgradingEv.defaultPrevented) { |
| 213 | return; |
| 214 | } |
| 215 | |
| 216 | var upgradedList = getUpgradedListOfElement_(element); |
| 217 | var classesToUpgrade = []; |
| 218 | // If jsClass is not provided scan the registered components to find the |
| 219 | // ones matching the element's CSS classList. |
| 220 | if (!optJsClass) { |
| 221 | var classList = element.classList; |
| 222 | registeredComponents_.forEach(function(component) { |
| 223 | // Match CSS & Not to be upgraded & Not upgraded. |
| 224 | if (classList.contains(component.cssClass) && |
| 225 | classesToUpgrade.indexOf(component) === -1 && |
| 226 | !isElementUpgraded_(element, component.className)) { |
| 227 | classesToUpgrade.push(component); |
| 228 | } |
| 229 | }); |
| 230 | } else if (!isElementUpgraded_(element, optJsClass)) { |
| 231 | classesToUpgrade.push(findRegisteredClass_(optJsClass)); |
| 232 | } |
| 233 | |
| 234 | // Upgrade the element for each classes. |
| 235 | for (var i = 0, n = classesToUpgrade.length, registeredClass; i < n; i++) { |
| 236 | registeredClass = classesToUpgrade[i]; |
| 237 | if (registeredClass) { |
| 238 | // Mark element as upgraded. |
| 239 | upgradedList.push(registeredClass.className); |
| 240 | element.setAttribute('data-upgraded', upgradedList.join(',')); |
| 241 | var instance = new registeredClass.classConstructor(element); |
| 242 | instance[componentConfigProperty_] = registeredClass; |
| 243 | createdComponents_.push(instance); |
| 244 | // Call any callbacks the user has registered with this component type. |
| 245 | for (var j = 0, m = registeredClass.callbacks.length; j < m; j++) { |
| 246 | registeredClass.callbacks[j](element); |
| 247 | } |
| 248 | |
| 249 | if (registeredClass.widget) { |
| 250 | // Assign per element instance for control over API |
| 251 | element[registeredClass.className] = instance; |
| 252 | } |
| 253 | } else { |
| 254 | throw new Error( |
| 255 | 'Unable to find a registered component for the given class.'); |
| 256 | } |
| 257 | |
| 258 | var upgradedEv = createEvent_('mdl-componentupgraded', true, false); |
| 259 | element.dispatchEvent(upgradedEv); |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | /** |
| 264 | * Upgrades a specific list of elements rather than all in the DOM. |
| 265 | * |
| 266 | * @param {!Element|!Array<!Element>|!NodeList|!HTMLCollection} elements |
| 267 | * The elements we wish to upgrade. |
| 268 | */ |
| 269 | function upgradeElementsInternal(elements) { |
| 270 | if (!Array.isArray(elements)) { |
| 271 | if (elements instanceof Element) { |
| 272 | elements = [elements]; |
| 273 | } else { |
| 274 | elements = Array.prototype.slice.call(elements); |
| 275 | } |
| 276 | } |
| 277 | for (var i = 0, n = elements.length, element; i < n; i++) { |
| 278 | element = elements[i]; |
| 279 | if (element instanceof HTMLElement) { |
| 280 | upgradeElementInternal(element); |
| 281 | if (element.children.length > 0) { |
| 282 | upgradeElementsInternal(element.children); |
| 283 | } |
| 284 | } |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * Registers a class for future use and attempts to upgrade existing DOM. |
| 290 | * |
| 291 | * @param {componentHandler.ComponentConfigPublic} config |
| 292 | */ |
| 293 | function registerInternal(config) { |
| 294 | // In order to support both Closure-compiled and uncompiled code accessing |
| 295 | // this method, we need to allow for both the dot and array syntax for |
| 296 | // property access. You'll therefore see the `foo.bar || foo['bar']` |
| 297 | // pattern repeated across this method. |
| 298 | var widgetMissing = (typeof config.widget === 'undefined' && |
| 299 | typeof config['widget'] === 'undefined'); |
| 300 | var widget = true; |
| 301 | |
| 302 | if (!widgetMissing) { |
| 303 | widget = config.widget || config['widget']; |
| 304 | } |
| 305 | |
| 306 | var newConfig = /** @type {componentHandler.ComponentConfig} */ ({ |
| 307 | classConstructor: config.constructor || config['constructor'], |
| 308 | className: config.classAsString || config['classAsString'], |
| 309 | cssClass: config.cssClass || config['cssClass'], |
| 310 | widget: widget, |
| 311 | callbacks: [] |
| 312 | }); |
| 313 | |
| 314 | registeredComponents_.forEach(function(item) { |
| 315 | if (item.cssClass === newConfig.cssClass) { |
| 316 | throw new Error('The provided cssClass has already been registered: ' + item.cssClass); |
| 317 | } |
| 318 | if (item.className === newConfig.className) { |
| 319 | throw new Error('The provided className has already been registered'); |
| 320 | } |
| 321 | }); |
| 322 | |
| 323 | if (config.constructor.prototype |
| 324 | .hasOwnProperty(componentConfigProperty_)) { |
| 325 | throw new Error( |
| 326 | 'MDL component classes must not have ' + componentConfigProperty_ + |
| 327 | ' defined as a property.'); |
| 328 | } |
| 329 | |
| 330 | var found = findRegisteredClass_(config.classAsString, newConfig); |
| 331 | |
| 332 | if (!found) { |
| 333 | registeredComponents_.push(newConfig); |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | /** |
| 338 | * Allows user to be alerted to any upgrades that are performed for a given |
| 339 | * component type |
| 340 | * |
| 341 | * @param {string} jsClass The class name of the MDL component we wish |
| 342 | * to hook into for any upgrades performed. |
| 343 | * @param {function(!HTMLElement)} callback The function to call upon an |
| 344 | * upgrade. This function should expect 1 parameter - the HTMLElement which |
| 345 | * got upgraded. |
| 346 | */ |
| 347 | function registerUpgradedCallbackInternal(jsClass, callback) { |
| 348 | var regClass = findRegisteredClass_(jsClass); |
| 349 | if (regClass) { |
| 350 | regClass.callbacks.push(callback); |
| 351 | } |
| 352 | } |
| 353 | |
| 354 | /** |
| 355 | * Upgrades all registered components found in the current DOM. This is |
| 356 | * automatically called on window load. |
| 357 | */ |
| 358 | function upgradeAllRegisteredInternal() { |
| 359 | for (var n = 0; n < registeredComponents_.length; n++) { |
| 360 | upgradeDomInternal(registeredComponents_[n].className); |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | /** |
| 365 | * Check the component for the downgrade method. |
| 366 | * Execute if found. |
| 367 | * Remove component from createdComponents list. |
| 368 | * |
| 369 | * @param {?componentHandler.Component} component |
| 370 | */ |
| 371 | function deconstructComponentInternal(component) { |
| 372 | if (component) { |
| 373 | var componentIndex = createdComponents_.indexOf(component); |
| 374 | createdComponents_.splice(componentIndex, 1); |
| 375 | |
| 376 | var upgrades = component.element_.getAttribute('data-upgraded').split(','); |
| 377 | var componentPlace = upgrades.indexOf(component[componentConfigProperty_].classAsString); |
| 378 | upgrades.splice(componentPlace, 1); |
| 379 | component.element_.setAttribute('data-upgraded', upgrades.join(',')); |
| 380 | |
| 381 | var ev = createEvent_('mdl-componentdowngraded', true, false); |
| 382 | component.element_.dispatchEvent(ev); |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | /** |
| 387 | * Downgrade either a given node, an array of nodes, or a NodeList. |
| 388 | * |
| 389 | * @param {!Node|!Array<!Node>|!NodeList} nodes |
| 390 | */ |
| 391 | function downgradeNodesInternal(nodes) { |
| 392 | /** |
| 393 | * Auxiliary function to downgrade a single node. |
| 394 | * @param {!Node} node the node to be downgraded |
| 395 | */ |
| 396 | var downgradeNode = function(node) { |
| 397 | createdComponents_.filter(function(item) { |
| 398 | return item.element_ === node; |
| 399 | }).forEach(deconstructComponentInternal); |
| 400 | }; |
| 401 | if (nodes instanceof Array || nodes instanceof NodeList) { |
| 402 | for (var n = 0; n < nodes.length; n++) { |
| 403 | downgradeNode(nodes[n]); |
| 404 | } |
| 405 | } else if (nodes instanceof Node) { |
| 406 | downgradeNode(nodes); |
| 407 | } else { |
| 408 | throw new Error('Invalid argument provided to downgrade MDL nodes.'); |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | // Now return the functions that should be made public with their publicly |
| 413 | // facing names... |
| 414 | return { |
| 415 | upgradeDom: upgradeDomInternal, |
| 416 | upgradeElement: upgradeElementInternal, |
| 417 | upgradeElements: upgradeElementsInternal, |
| 418 | upgradeAllRegistered: upgradeAllRegisteredInternal, |
| 419 | registerUpgradedCallback: registerUpgradedCallbackInternal, |
| 420 | register: registerInternal, |
| 421 | downgradeElements: downgradeNodesInternal |
| 422 | }; |
| 423 | })(); |
| 424 | |
| 425 | /** |
| 426 | * Describes the type of a registered component type managed by |
| 427 | * componentHandler. Provided for benefit of the Closure compiler. |
| 428 | * |
| 429 | * @typedef {{ |
| 430 | * constructor: Function, |
| 431 | * classAsString: string, |
| 432 | * cssClass: string, |
| 433 | * widget: (string|boolean|undefined) |
| 434 | * }} |
| 435 | */ |
| 436 | componentHandler.ComponentConfigPublic; // jshint ignore:line |
| 437 | |
| 438 | /** |
| 439 | * Describes the type of a registered component type managed by |
| 440 | * componentHandler. Provided for benefit of the Closure compiler. |
| 441 | * |
| 442 | * @typedef {{ |
| 443 | * constructor: !Function, |
| 444 | * className: string, |
| 445 | * cssClass: string, |
| 446 | * widget: (string|boolean), |
| 447 | * callbacks: !Array<function(!HTMLElement)> |
| 448 | * }} |
| 449 | */ |
| 450 | componentHandler.ComponentConfig; // jshint ignore:line |
| 451 | |
| 452 | /** |
| 453 | * Created component (i.e., upgraded element) type as managed by |
| 454 | * componentHandler. Provided for benefit of the Closure compiler. |
| 455 | * |
| 456 | * @typedef {{ |
| 457 | * element_: !HTMLElement, |
| 458 | * className: string, |
| 459 | * classAsString: string, |
| 460 | * cssClass: string, |
| 461 | * widget: string |
| 462 | * }} |
| 463 | */ |
| 464 | componentHandler.Component; // jshint ignore:line |
| 465 | |
| 466 | // Export all symbols, for the benefit of Closure compiler. |
| 467 | // No effect on uncompiled code. |
| 468 | componentHandler['upgradeDom'] = componentHandler.upgradeDom; |
| 469 | componentHandler['upgradeElement'] = componentHandler.upgradeElement; |
| 470 | componentHandler['upgradeElements'] = componentHandler.upgradeElements; |
| 471 | componentHandler['upgradeAllRegistered'] = |
| 472 | componentHandler.upgradeAllRegistered; |
| 473 | componentHandler['registerUpgradedCallback'] = |
| 474 | componentHandler.registerUpgradedCallback; |
| 475 | componentHandler['register'] = componentHandler.register; |
| 476 | componentHandler['downgradeElements'] = componentHandler.downgradeElements; |
| 477 | window.componentHandler = componentHandler; |
| 478 | window['componentHandler'] = componentHandler; |
| 479 | |
| 480 | window.addEventListener('load', function() { |
| 481 | 'use strict'; |
| 482 | |
| 483 | /** |
| 484 | * Performs a "Cutting the mustard" test. If the browser supports the features |
| 485 | * tested, adds a mdl-js class to the <html> element. It then upgrades all MDL |
| 486 | * components requiring JavaScript. |
| 487 | */ |
| 488 | if ('classList' in document.createElement('div') && |
| 489 | 'querySelector' in document && |
| 490 | 'addEventListener' in window && Array.prototype.forEach) { |
| 491 | document.documentElement.classList.add('mdl-js'); |
| 492 | componentHandler.upgradeAllRegistered(); |
| 493 | } else { |
| 494 | /** |
| 495 | * Dummy function to avoid JS errors. |
| 496 | */ |
| 497 | componentHandler.upgradeElement = function() {}; |
| 498 | /** |
| 499 | * Dummy function to avoid JS errors. |
| 500 | */ |
| 501 | componentHandler.register = function() {}; |
| 502 | } |
| 503 | }); |