blob: f532a98879731543c7311dcb3c157050ebb7ca91 [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001/**
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.
29var 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
85componentHandler = (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 */
436componentHandler.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 */
450componentHandler.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 */
464componentHandler.Component; // jshint ignore:line
465
466// Export all symbols, for the benefit of Closure compiler.
467// No effect on uncompiled code.
468componentHandler['upgradeDom'] = componentHandler.upgradeDom;
469componentHandler['upgradeElement'] = componentHandler.upgradeElement;
470componentHandler['upgradeElements'] = componentHandler.upgradeElements;
471componentHandler['upgradeAllRegistered'] =
472 componentHandler.upgradeAllRegistered;
473componentHandler['registerUpgradedCallback'] =
474 componentHandler.registerUpgradedCallback;
475componentHandler['register'] = componentHandler.register;
476componentHandler['downgradeElements'] = componentHandler.downgradeElements;
477window.componentHandler = componentHandler;
478window['componentHandler'] = componentHandler;
479
480window.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});