Source: delite/CustomElement.js

/** @module delite/CustomElement */
define([
	"dcl/dcl",
	"dojo/_base/lang",
	"dojo/dom-construct", // domConstruct.destroy
	"dojo/on",
	"./Destroyable",
	"./Stateful"
], function (dcl, lang, domConstruct, on, Destroyable, Stateful) {

	// module:
	//		delite/CustomElement

	var div = document.createElement("div");

	/**
	 * @summary
	 * Base class for all custom elements.
	 * @description
	 * Use this class rather that Widget for non-visual custom elements.
	 * Custom elements can provide custom setters/getters for properties, which are called automatically
	 * when the value is set.  For an attribute XXX, define methods _setXXXAttr() and/or _getXXXAttr()
	 * @class module:delite/CustomElement
	 * @augments {module:delite/Stateful}
	 * @augments {module:delite/Destroyable}
	 */
	return dcl([Stateful, Destroyable], /** @lends module:delite/CustomElement# */{
		// summary:
		//		Base class for all custom elements.
		//		Use this class rather that Widget for non-visual custom elements.
		//
		//		Custom elements can provide custom setters/getters for properties, which are called automatically
		//		when the value is set.  For an attribute XXX, define methods _setXXXAttr() and/or _getXXXAttr().

		_getProps: function () {
			// Override _Stateful._getProps() to ignore properties from the HTML*Element superclasses, like "style".
			// You would need to explicitly declare style: "" in your widget to get it here.
			// Intentionally skips privates and methods, because it seems wasteful to have a custom
			// setter for every method; not sure that would work anyway.
			//
			// Also sets up this._propCaseMap, a mapping from lowercase property name to actual name,
			// ex: iconclass --> iconClass, which does include the methods, but again doesn't
			// include props like "style" that are merely inherited from HTMLElement.

			var list = [], proto = this, ctor,
				pcm = this._propCaseMap = {};

			do {
				Object.keys(proto).forEach(function (prop) {
					if (!/^_/.test(prop)) {
						if (typeof proto[prop] !== "function") {
							list.push(prop);
						}
						pcm[prop.toLowerCase()] = prop;
					}
				});

				proto = Object.getPrototypeOf(proto);
				ctor = proto && proto.constructor;
			} while (proto && ctor !== this._baseElement);

			return list;
		},

		/**
		 * Some doc. 
		 */
		createdCallback: dcl.advise({
			before: function () {
				// Get parameters that were specified declaratively on the widget DOMNode.
				this._declaredParams = this._mapAttributes();

				// FF has a native watch() method that overrides our Stateful.watch() method and breaks custom setters,
				// so that any command like this.label = "hello" sets label to undefined instead.  Try to workaround.
				this.watch = Stateful.prototype.watch;
			},

			after: function () {
				this._created = true;

				// Now that creation has finished, apply parameters that were specified declaratively.
				// This is consistent with the timing that parameters are applied for programmatic creation.
				dcl.mix(this, this._declaredParams);
			}
		}),

		_mapAttributes: function () {
			// summary:
			//		Get declaratively specified attributes to widget properties
			var pcm = this._propCaseMap,
				attr,
				idx = 0,
				props = {};

			// inner functions useful to reduce cyclomatic complexity when using jshint
			function stringToObject(value) {
				var obj;

				try {
					// TODO: remove this code if it isn't being used, so we don't scare people that are afraid of eval.
					/* jshint evil:true */
					// This will only be executed when complex parameters are used in markup
					// <my-tag constraints="max: 3, min: 2"></my-tag>
					// This can be avoided by using such complex parameters only programmatically or by not using
					// them at all.
					// This is harmless if you make sure the JavaScript code that is passed to the attribute
					// is harmless.
					obj = eval("(" + (value[0] === "{" ? "" : "{") + value + (value[0] === "{" ? "" : "}") + ")");
				}
				catch (e) {
					throw new SyntaxError("Error in attribute conversion to object: " + e.message +
						"\nAttribute Value: '" + value + "'");
				}
				return obj;
			}

			function setTypedValue(widget, name, value) {
				switch (typeof widget[name]) {
				case "string":
					props[name] = value;
					break;
				case "number":
					props[name] = value - 0;
					break;
				case "boolean":
					props[name] = value !== "false";
					break;
				case "object":
					var obj = lang.getObject(value, false);
					if (obj) {
						// it's a global, ex: store="myStore"
						props[name] = obj;
					} else {
						// it's an expression, ex: constraints="min: 10, max: 100"
						props[name] = (widget[name] instanceof Array)
							? (value
							? value.split(/\s+/)
							: [])
							: stringToObject(value);
					}
					break;
				case "function":
					/* jshint evil:true */
					// This will only be executed if you have properties that are of function type if your widget
					// and that you set them in your tag attributes:
					// <my-tag whatever="myfunc"></my-tag>
					// This can be avoided by setting the function progammatically or by not setting it at all.
					// This is harmless if you make sure the JavaScript code that is passed to the attribute
					// is harmless.
					props[name] = lang.getObject(value, false) || new Function(value);
				}
				delete widget[name]; // make sure custom setters fire
			}

			var attrsToRemove = [];
			while ((attr = this.attributes[idx++])) {
				// Map all attributes except for things like onclick="..." since the browser already handles them.
				var name = attr.name.toLowerCase();	// note: will be lower case already except for IE9
				if (name in pcm) {
					setTypedValue(this, pcm[name]/* convert to correct case for widget */, attr.value);
					attrsToRemove.push(name);
				}
			}

			// Remove attributes that were processed, but do it in a separate loop so we don't modify this.attributes
			// while we are looping through it.   (See CustomElement-attr.html test failure on IE10.)
			attrsToRemove.forEach(this.removeAttribute, this);

			return props;
		},

		destroy: function () {
			// summary:
			//		Release resources used by this custom element and its descendants.
			//		After calling this method, the element can no longer be used,
			//		and should be removed from the document.

			// Destroy descendants
			this.findCustomElements(this).forEach(function (w) {
				if (w.destroy) {
					w.destroy();
				}
			});

			// Destroy this
			domConstruct.destroy(this);
		},

		/**
		 * @summary
		 * Signal that a synthetic event occurred, ex:
		 * <pre><code>
		 * myWidget.emit("attrmodified-selectedChildWidget", {}).
		 * </code></pre>
		 * @description
		 * Emits an event of specified type, based on eventObj.
		 * Also calls onType() method, if present, and returns value from that method.
		 * Modifies eventObj by adding missing parameters (bubbles, cancelable, widget).
		 */
		emit: function (/*String*/ type, /*Object?*/ eventObj) {
			// summary:
			//		Signal that a synthetic event occurred, ex:
			//	|	myWidget.emit("attrmodified-selectedChildWidget", {}).
			//
			//		Emits an event of specified type, based on eventObj.
			//		Also calls onType() method, if present, and returns value from that method.
			//		Modifies eventObj by adding missing parameters (bubbles, cancelable, widget).
			// tags:
			//		protected

			// Specify fallback values for bubbles, cancelable in case they are not set in eventObj.
			// Also set pointer to widget, although since we can't add a pointer to the widget for native events
			// (see #14729), maybe we shouldn't do it here?
			eventObj = eventObj || {};
			if (eventObj.bubbles === undefined) {
				eventObj.bubbles = true;
			}
			if (eventObj.cancelable === undefined) {
				eventObj.cancelable = true;
			}

			// Emit event, but (for the case of the Widget subclass)
			// avoid spurious emit()'s as parent sets properties on child during startup/destroy
			if (this._started !== false && !this._beingDestroyed) {
				// Call onType() method if one exists.   But skip functions like onchange and onclick
				// because the browser will call them automatically when the event is emitted.
				var ret, callback = this["on" + type];
				if (callback && !("on" + type.toLowerCase() in div)) {
					ret = callback.call(this, eventObj);
				}

				// Emit the event
				on.emit(this, type, eventObj);
			}

			return ret;
		},

		/**
		 * Call specified function when event occurs, ex: myWidget.on("click", function () { ... }).
		 */
		on: function (/*String|Function*/ type, /*Function*/ func) {
			// summary:
			//		Call specified function when event occurs, ex: myWidget.on("click", function () { ... }).
			// type:
			//		Name of event (ex: "click") or extension event like touch.press.
			// description:
			//		Call specified function when event `type` occurs, ex: `myWidget.on("click", function () { ... })`.
			//		Note that the function is not run in any particular scope, so if (for example) you want it to run
			//		in the widget's scope you must do `myWidget.on("click", myWidget.func.bind(myWidget))`.

			return this.own(on(this, type, func))[0];
		},

		/**
		 * Wrapper to setTimeout to avoid deferred functions executing
		 * after the originating widget has been destroyed.
		 * Returns an object handle with a remove method (that returns null) (replaces clearTimeout).
 		 * @param {Object} fcn Function reference.
 		 * @param {Object} delay Delay, defaults to 0.
		 */
		defer: function (fcn, delay) {
			// summary:
			//		Wrapper to setTimeout to avoid deferred functions executing
			//		after the originating widget has been destroyed.
			//		Returns an object handle with a remove method (that returns null) (replaces clearTimeout).
			// fcn: Function
			//		Function reference.
			// delay: Number?
			//		Delay, defaults to 0.
			// tags:
			//		protected

			var timer = setTimeout(
				(function () {
					if (!timer) {
						return;
					}
					timer = null;
					if (!this._destroyed) {
						lang.hitch(this, fcn)();
					}
				}).bind(this),
				delay || 0
			);
			return {
				remove: function () {
					if (timer) {
						clearTimeout(timer);
						timer = null;
					}
					return null; // so this works well: handle = handle.remove();
				}
			};
		},

		// Utility functions previously in registry.js

		/**
		 * Search subtree under root returning custom elements found.
 		 * @param [Element] root Node to search under.
		 */
		findCustomElements: function (root) {
			// summary:
			//		Search subtree under root returning custom elements found.
			// root: Element?
			//		Node to search under.

			// TODO: In dijit this didn't search for nested widgets (ie: widgets inside other widgets).

			var outAry = [];

			function getChildrenHelper(root) {
				for (var node = root.firstChild; node; node = node.nextSibling) {
					if (node.nodeType === 1 && node.createdCallback) {
						outAry.push(node);
					} else {
						getChildrenHelper(node);
					}
				}
			}

			getChildrenHelper(root || this.ownerDocument.body);
			return outAry;
		}
	});
});