Source: delite/register.js

/** @module delite/register */
define([
	"module",
	"dcl/advise",
	"dcl/dcl",
	"decor/schedule",
	"requirejs-domready/domReady",	// loading as a function, not as a plugin
	"./features"
], function (module, advise, dcl, schedule, domReady, has) {
	"use strict";

	var doc = has("builder") ? require.nodeRequire(require.getNodePath(require.toUrl(module.id).replace(/[^\/]*$/,
		"node_modules/jsdom"))).jsdom("") : document;

	// Set to true after the page finishes loading and the parser runs.  Any widgets declared after initialParseComplete
	// instantiated in a separate code path.
	var initialParseComplete;

	// Workaround problem using dcl() on native DOMNodes on FF and IE,
	// see https://github.com/uhop/dcl/issues/9.
	// Fixes case where tabIndex is declared in a mixin that's passed to register().
	dcl.mix = function (a, b) {
		for (var n in b) {
			try {
				a[n] = b[n];
			} catch (e) {
				Object.defineProperty(a, n, {
					configurable: true,
					writable: true,
					enumerable: true,
					value: b[n]
				});
			}
		}
	};

	/**
	 * List of selectors that the parser needs to search for as possible upgrade targets.  Mainly contains
	 * the widget custom tags like d-accordion, but also selectors like button[is='d-button'] to find <button is="...">
	 * @type {string[]}
	 */
	var selectors = [];

	/**
	 * Internal registry of widget class metadata.
	 * Key is custom widget tag name, used as Element tag name like <d-accordion> or "is" attribute like
	 * <button is="d-accordion">).
	 * Value is metadata about the widget, including its prototype, ex: {prototype: object, extends: "button", ... }
	 * @type {Object}
	 */
	var registry = {};

	/**
	 * Create an Element.  Similar to document.createElement(), but if tag is the name of a widget defined by
	 * register(), then it upgrades the Element to be a widget.
	 * @function module:delite/register.createElement
	 * @param {string} tag
	 * @returns {Element} The DOMNode
	 */
	function createElement(tag) {
		if (/-/.test(tag) && !(tag in registry) && !has("builder")) {
			// Try to help people that have templates with custom elements but they forgot to do requires="..."
			console.warn("register.createElement(): undefined tag '" + tag +
				"', did you forget requires='...' in your template?");
		}

		var base = registry[tag] ? registry[tag].extends : null;
		if (has("document-register-element")) {
			return base ? doc.createElement(base, tag) : doc.createElement(tag);
		} else {
			var element = doc.createElement(base || tag);
			if (base) {
				element.setAttribute("is", tag);
			}
			upgrade(element);
			return element;
		}
	}

	/**
	 * Generate metadata about all the properties in proto, both direct and inherited.
	 * On IE<=10, these properties will be applied to a DOMNode via Object.defineProperties().
	 * Skips properties in the base element (HTMLElement, HTMLButtonElement, etc.)
	 * @param {Object} proto - The prototype.
	 * @returns {Object} Hash from property name to return value from `Object.getOwnPropertyDescriptor()`.
	 */
	function getPropDescriptors(proto) {
		var props = {};

		do {
			var keys = Object.getOwnPropertyNames(proto);	// better than Object.keys() because finds hidden props too
			for (var i = 0, k; (k = keys[i]); i++) {
				if (!props[k]) {
					props[k] = Object.getOwnPropertyDescriptor(proto, k);
				}
			}
			proto = Object.getPrototypeOf(proto);
		} while (!/HTML[a-zA-Z]*Element/.test(proto.constructor.toString()));

		return props;
	}

	/**
	 * Converts plain Element of custom type into "custom element", by adding the widget's custom methods, etc.
	 * Does nothing if the Element has already been converted or if it doesn't correspond to a registered custom tag.
	 * After the upgrade, calls `createdCallback()`.
	 *
	 * Usually the application will not need to call this method directly, because it's called automatically
	 * on page load and as elements are added to the document.
	 *
	 * @function module:delite/register.upgrade
	 * @param {Element} element - The DOM node.
	 * @param {boolean} [attach] - If `element`'s tag has been registered, but `attachedCallback()` hasn't yet been
	 * called [since the last call to `detachedCallback()`], then call `attachedCallback()`.  Call even if the element
	 * has already been upgraded.
	 */
	function upgrade(element, attach) {
		if (!has("document-register-element")) {
			var widget = registry[element.getAttribute("is") || element.nodeName.toLowerCase()];
			if (widget) {
				if (!element._upgraded) {
					if (has("dom-proto-set")) {
						// Redefine Element's prototype to point to widget's methods etc.
						/*jshint camelcase: false*/
						/*jshint proto: true*/
						element.__proto__ = widget.prototype;
						/*jshint camelcase: true*/
						/*jshint proto: false*/
					} else {
						// Mixin all the widget's methods etc. into Element
						Object.defineProperties(element, widget.props);
					}
					element._upgraded = true;
					if (element.createdCallback) {
						element.createdCallback();
					}
				}
				if (attach && !element._attached) {
					element.attachedCallback();
				}
			}
		}
	}

	/**
	 * Call detachedCallback() on specified Element if it's a custom element that was upgraded by us.
	 * @param {Element} node
	 */
	function detach(node) {
		if (node._upgraded) {
			node.detachedCallback();
		}
	}

	/**
	 * Mapping of tag names to HTMLElement interfaces.
	 * Doesn't include newer elements not available on all browsers.
	 * @type {Object}
	 */
	var tagMap = typeof HTMLElement !== "undefined" && {	// "typeof HTMLElement" check so module loads in NodeJS
		a: HTMLAnchorElement,
		// applet: HTMLAppletElement,
		// area: HTMLAreaElement,
		// audio: HTMLAudioElement,
		base: HTMLBaseElement,
		br: HTMLBRElement,
		button: HTMLButtonElement,
		canvas: HTMLCanvasElement,
		// data: HTMLDataElement,
		// datalist: HTMLDataListElement,
		div: HTMLDivElement,
		dl: HTMLDListElement,
		directory: HTMLDirectoryElement,
		// embed: HTMLEmbedElement,
		fieldset: HTMLFieldSetElement,
		font: HTMLFontElement,
		form: HTMLFormElement,
		head: HTMLHeadElement,
		h1: HTMLHeadingElement,
		html: HTMLHtmlElement,
		hr: HTMLHRElement,
		iframe: HTMLIFrameElement,
		img: HTMLImageElement,
		input: HTMLInputElement,
		// keygen: HTMLKeygenElement,
		label: HTMLLabelElement,
		legend: HTMLLegendElement,
		li: HTMLLIElement,
		link: HTMLLinkElement,
		map: HTMLMapElement,
		// media: HTMLMediaElement,
		menu: HTMLMenuElement,
		meta: HTMLMetaElement,
		// meter: HTMLMeterElement,
		ins: HTMLModElement,
		object: HTMLObjectElement,
		ol: HTMLOListElement,
		optgroup: HTMLOptGroupElement,
		option: HTMLOptionElement,
		// output: HTMLOutputElement,
		p: HTMLParagraphElement,
		param: HTMLParamElement,
		pre: HTMLPreElement,
		// progress: HTMLProgressElement,
		quote: HTMLQuoteElement,
		script: HTMLScriptElement,
		select: HTMLSelectElement,
		// source: HTMLSourceElement,
		// span: HTMLSpanElement,
		style: HTMLStyleElement,
		table: HTMLTableElement,
		caption: HTMLTableCaptionElement,
		// td: HTMLTableDataCellElement,
		// th: HTMLTableHeaderCellElement,
		col: HTMLTableColElement,
		tr: HTMLTableRowElement,
		tbody: HTMLTableSectionElement,
		textarea: HTMLTextAreaElement,
		// time: HTMLTimeElement,
		title: HTMLTitleElement,
		// track: HTMLTrackElement,
		ul: HTMLUListElement,
		// blink: HTMLUnknownElement,
		video: HTMLVideoElement
	};
	var tags = tagMap && Object.keys(tagMap);

	/**
	 * Registers the tag with the current document, and save tag information in registry.
	 * Handles situations where the base constructor inherits from
	 * HTMLElement but is not HTMLElement.
	 * @param  {string}   tag         The custom tag name for the element, or the "is" attribute value.
	 * @param  {string}	  _extends    The name of the tag this element extends, ex: "button" for <button is="...">
	 * @param  {string}   baseElement The native HTML*Element "class" that this custom element is extending.
	 * @param  {Function} baseCtor    The constructor function.
	 * @return {Function}             The "new" constructor function that can create instances of the custom element.
	 */
	function getTagConstructor(tag, _extends, baseElement, baseCtor) {
		var proto = baseCtor.prototype,
			config = registry[tag] = {
				constructor: baseCtor,
				prototype: proto
			};

		if (_extends) {
			config.extends = _extends;
		}

		if (has("document-register-element")) {
			doc.registerElement(tag, config);
		} else {
			if (!has("dom-proto-set")) {
				// Get descriptors for all the properties in the prototype.  This is needed on IE<=10 in upgrade().
				config.props = getPropDescriptors(proto);
			}
		}

		// Note: if we wanted to support registering new types after the parser was called, then here we should
		// scan the document for the new type (selectors[length-1]) and upgrade any nodes found.

		// Create a constructor method to return a DOMNode representing this widget.
		var tagConstructor = function (params) {
			// Create new widget node or upgrade existing node to widget
			var node = createElement(tag);

			// Set parameters on node
			for (var name in params || {}) {
				if (name === "style") {
					node.style.cssText = params.style;
				} else if ((name === "class" || name === "className") && node.setClassComponent) {
					node.setClassComponent("user", params[name]);
				} else {
					node[name] = params[name];
				}
			}
			if (node.deliver) {
				node.deliver();
			}

			return node;
		};

		// Add some flags for debugging and return the new constructor
		tagConstructor.tag = tag;
		tagConstructor._ctor = baseCtor;

		// Register the selector to find this custom element
		var selector = _extends ? _extends + "[is='" + tag + "']" : tag;
		selectors.push(selector);

		// If the document has already been parsed then do a supplementary sweep for this new custom element.
		if (initialParseComplete && !has("document-register-element")) {
			unobserve();	// pause listening for added/deleted nodes
			var node, idx = 0, nodes = doc.querySelectorAll(selector);
			while ((node = nodes[idx++])) {
				upgrade(node, true);
			}
			observe();	// resume listening for added/deleted nodes
		}

		return tagConstructor;
	}

	/**
	 * Restore the "true" constructor when trying to recombine custom elements
	 * @param  {Function} extension A constructor function that might have a shadow property that contains the
	 *                              original constructor
	 * @return {Function}           The original construction function or the existing function/object
	 */
	function restore(extension) {
		return (extension && extension._ctor) || extension;
	}

	/**
	 * Declare a widget and register it as a custom element.
	 *
	 * props{} can provide custom setters/getters for widget properties, which are called automatically when
	 * the widget properties are set.
	 * For a property XXX, define methods _setXXXAttr() and/or _getXXXAttr().
	 *
	 * @param  {string}               tag             The custom element's tag name.
	 * @param  {Object[]}             superclasses    Any number of superclasses to be built into the custom element
	 *                                                constructor. But first one must be [descendant] of HTMLElement.
	 * @param  {Object}               props           Properties of this widget class.
	 * @return {Function}                             A constructor function that will create an instance of the custom
	 *                                                element.
	 * @function module:delite/register
	 */
	function register(tag, superclasses, props) {
		// Create the widget class by extending specified superclasses and adding specified properties.

		// Make sure all the bases have their proper constructors for being composited.
		// I.E. remove the wrapper added by getTagConstructor().
		var bases = (superclasses instanceof Array ? superclasses : superclasses ? [superclasses] : []).map(restore);


		// Check to see if the custom tag is already registered
		if (tag in registry) {
			throw new TypeError("A widget is already registered with tag '" + tag + "'.");
		}

		// Get root (aka native) class: HTMLElement, HTMLInputElement, etc.
		var baseElement = bases[0];
		if (baseElement.prototype && baseElement.prototype._baseElement) {
			// The first superclass is a widget created by another call to register, so get that widget's root class
			baseElement = baseElement.prototype._baseElement;
		}

		// Get name of tag that this widget extends, for example <button is="..."> --> "button"
		var _extends;
		if (baseElement !== HTMLElement) {
			_extends = tags.filter(function (tag) {
				return tagMap[tag] === baseElement;
			})[0];
			if (!_extends) {
				throw new TypeError(tag + ": must have HTMLElement in prototype chain");
			}
		}

		// Get a composited constructor
		var ctor = dcl(bases, props || {}),
			proto = ctor.prototype;
		proto._ctor = ctor;
		proto._baseElement = baseElement;
		proto._tag = tag;
		proto._extends = _extends;

		// Monkey-patch attachedCallback() and detachedCallback() to avoid double executions.
		// Generally this isn't an issue, but it could happen if the app manually called the functions
		// and then they were called automatically too.
		advise.around(proto, "attachedCallback", function (sup) {
			return function () {
				if (this._attached) { return; }
				if (sup) { sup.apply(this, arguments); }
				this._attached = true;
			};
		});
		advise.around(proto, "detachedCallback", function (sup) {
			return function () {
				if (!this._attached) { return; }
				if (sup) { sup.apply(this, arguments); }
				this._attached = false;
			};
		});

		// Run introspection to add ES5 getters/setters.
		// Doesn't happen automatically because Stateful's constructor isn't called.
		// Also, on IE this needs to happen before the getTagConstructor() call,
		// since getTagConstructor() scans all the properties on the widget prototype.
		if (proto.introspect) {
			ctor._propsToObserve = proto.getProps();
			proto.introspect(ctor._propsToObserve);
			ctor.introspected = true;
		}

		// Save widget metadata to the registry and return constructor that creates an upgraded DOMNode for the widget
		/* jshint boss:true */
		return getTagConstructor(tag, _extends, baseElement, ctor);
	}

	/**
	 * Parse the given DOM tree for any Elements that need to be upgraded to widgets.
	 * Searches all descendants of the specified node, but does not upgrade the node itself.
	 *
	 * Usually the application will not need to call this method directly, because it's called automatically
	 * on page load and as elements are added to the document.
	 *
	 * @function module:delite/register.parse
	 * @param {Element} [root] DOM node to parse from.
	 */
	function parse(root) {
		if (!has("document-register-element") && selectors.length) {
			var node, idx = 0, nodes = (root || doc).querySelectorAll(selectors.join(", "));
			while ((node = nodes[idx++])) {
				upgrade(node, true);
			}
		}
	}

	// ------------------------
	// Code to listen for nodes being added/deleted from the document, to automatically call parse()/detachedCallback()
	var observer;

	/**
	 * Start listening for added/deleted nodes.
	 */
	function observe() {
		if (!has("document-register-element")) {
			if (!observer) {
				if (has("MutationObserver")) {
					observer = new MutationObserver(processMutations);
				} else {
					// Fallback for Android < 4.2 and IE < 11.  Partial shim of MutationObserver, except sometimes
					// addedNodes lists all nodes not just the root of each added tree.
					observer = {
						takeRecords: function () {
							var ret = this._mutations;
							this._mutations = [];
							if (this._timer) {
								this._timer.remove();
								this._timer = null;
							}
							return ret;
						},
						observe: function () {
							this._mutations = [];
							this._listener = function (event) {
								if (event.target.nodeType === 1) {
									var mutation = {};
									mutation[event.type === "DOMNodeInserted" ? "addedNodes" : "removedNodes"] =
										[event.target];
									this._mutations.push(mutation);
								}
								if (!this._timer) {
									this._timer = schedule(function () {
										this._timer = null;
										processMutations(this.takeRecords());
									}.bind(this));
								}
							}.bind(this);
							doc.body.addEventListener("DOMNodeInserted", this._listener);
							doc.body.addEventListener("DOMNodeRemoved", this._listener);
						},
						disconnect: function () {
							doc.body.removeEventListener("DOMNodeInserted", this._listener);
							doc.body.removeEventListener("DOMNodeRemoved", this._listener);
						}
					};
				}
			}
			observer.observe(doc.body, {childList: true, subtree: true});
		}
	}

	/**
	 * Stop (aka pause) listening for added/deleted nodes.
	 */
	function unobserve() {
		if (observer) {
			observer.disconnect();
		}
	}

	/**
	 * Process the added/deleted nodes.  Called for incremental updates after initial parse.
	 * @param mutations
	 */
	function processMutations(mutations) {
		if (!has("document-register-element") && selectors.length) {
			unobserve();	// pause listening for added/deleted nodes
			var parseDescendants = has("MutationObserver") || has("DOMNodeInserted") === "root";
			mutations.forEach(function (mutation) {
				var added, idx1 = 0;
				while ((added = mutation.addedNodes && mutation.addedNodes[idx1++])) {
					// contains() checks avoid calling attachedCallback() on nodes not attached to document because:
					//		1. node was added then removed before processMutations() was called
					//		2. node was added and then its ancestor was removed before processMutations() was called
					if (added.nodeType === 1 && added.ownerDocument.body.contains(added)) {
						// upgrade the node itself (if it's a custom widget and it hasn't been upgraded yet),
						// and then call attachedCallback() on it
						upgrade(added, true);

						// upgrade any descendants that are custom widgets (if they aren't already upgraded),
						// and then call attachedCallback() on them
						if (parseDescendants) {
							parse(added);
						}
					}
				}

				var removedRoot, idx2 = 0;
				while ((removedRoot = mutation.removedNodes && mutation.removedNodes[idx2++])) {
					if (removedRoot.nodeType === 1) {
						detach(removedRoot);
						var removed, idx3 = 0, removedDescendants = removedRoot.querySelectorAll(selectors.join(", "));
						while ((removed = removedDescendants[idx3++])) {
							detach(removed);
						}
					}
				}
			});
			observe();	// resume listening for added/deleted nodes
		}
	}

	/**
	 * Upgrade any custom tags in the document that have not yet been upgraded.
	 * Nodes are automatically updated asynchronously, but applications can synchronously update them by calling
	 * this method.  Should not be called before domReady event.
	 */
	function deliver() {
		if (!has("document-register-element")) {
			if (!initialParseComplete) {
				parse();
				initialParseComplete = true;
				observe();
			} else {
				processMutations(observer.takeRecords());
			}
		}
	}

	// Setup initial parse of document and also listeners for future document modifications.
	if (!has("document-register-element") && doc) {
		domReady(function () {
			if (!has("dom-template")) {
				// Move <template> child nodes to .content property, so that we don't parse custom elements in
				// <template> nodes.  Could be done on dynamically created nodes too, but currently there's no need.
				var template, idx = 0, nodes = doc.querySelectorAll("template");
				while ((template = nodes[idx++])) {
					if (!template.content) {
						var child, content = template.content = doc.createDocumentFragment();
						while ((child = template.firstChild)) {
							content.appendChild(child);
						}
					}
				}
			}

			// Upgrade all custom element nodes, and setup listeners for future changes.
			deliver();
		});
	}

	// Setup return value as register() method, with other methods hung off it.
	register.upgrade = upgrade;
	register.createElement = createElement;
	register.parse = parse;
	register.deliver = deliver;

	// Add helpers from dcl for declaring classes.

	/**
	 * Convenience shortcut to [dcl()](http://www.dcljs.org/docs/mini_js/dcl/).
	 * @function module:delite/register.dcl
	 */
	register.dcl = dcl;

	/**
	 * Convenience shortcut to [dcl.after()](http://www.dcljs.org/docs/dcl_js/after/).
	 * @function module:delite/register.after
	 */
	register.after = dcl.after;

	/**
	 * Convenience shortcut to [dcl.before()](http://www.dcljs.org/docs/dcl_js/before/).
	 * @function module:delite/register.before
	 */
	register.before = dcl.before;

	/**
	 * Convenience shortcut to [dcl.around()](http://www.dcljs.org/docs/dcl_js/around/).
	 * @function module:delite/register.around
	 */
	register.around = dcl.around;

	/**
	 * Convenience shortcut to [dcl.superCall()](http://www.dcljs.org/docs/mini_js/supercall/).
	 * @function module:delite/register.superCall
	 */
	register.superCall = dcl.superCall;

	return register;
});