Source: utils/kekule.domUtils.js

/**
 * @fileoverview
 * Utility functions about DOM, Script and so on.
 * @author Partridge Jiang
 */

(function(){

/*
if (typeof(Kekule) === 'undefined')
{
	var Kekule = {};
}
*/


/**
 *  An class with static methods handle HTML or XML DOM.
 *  @object
 */
Kekule.DomUtils = {
	/**
	 * Check if node.namespaceURI == namespaceURI or no namespace info on node
	 * @param {Object} node
	 * @param {String} namespaceURI
	 * @param {Bool} allowEmptyNamespace Whether return true if node.namespaceURI is empty
	 * @returns {Bool}
	 */
	namespaceMatched: function(node, namespaceURI, allowEmptyNamespace)
	{
		if (allowEmptyNamespace === undefined)
			allowEmptyNamespace = true;
		return ((node.namespaceURI == namespaceURI) || (allowEmptyNamespace && (!node.namespaceURI)));
	},
	/**
	 * Check if an object is element.
	 * @param {Variant} obj
	 */
	isElement: function(obj)
	{
		return (obj && obj.tagName && obj.nodeType);
	},
	/**
	 * Get text content of an element.
	 *  This function will not consider child elements, just direct text.
	 */
	getElementText: function(elem)
	{
		var result = '';
		for (var i = 0, l = elem.childNodes.length; i < l; ++i)
		{
			if (elem.childNodes[i].nodeType == 3)  // Node.TEXT_NODE
				result += elem.childNodes[i].nodeValue;
		}
		return result;
	},
	/**
	 * Set text content of an element.
	 *  This function will not consider child elements, just replace current first text node or
	 *  append a new text node to the tail of element's children.
	 */
	setElementText: function(elem, text)
	{
		var result = '';
		for (var i = 0, l = elem.childNodes.length; i < l; ++i)
		{
			if (elem.childNodes[i].nodeType == 3) // Node.TEXT_NODE
			{
				elem.childNodes[i].nodeValue = text;
				return text;
			}
		}
		// no text node, append a new one
		var textNode = elem.ownerDocument.createTextNode(text);
		elem.appendChild(textNode);
		return text;
	},
	/**
	 * Get local name of element.
	 * In standard browser, this function should return elem.localName. However,
	 * IE (<= 8) does not implement localName property, so we have to check baseName
	 * or nodeName instead.
	 * @param {Object} elemOrAttrib
	 * @returns {String}
	 */
	getLocalName: function(elemOrAttrib)
	{
		return elemOrAttrib.localName || elemOrAttrib.baseName || elemOrAttrib.nodeName;
	},
	/**
	 * Returns a list of child nodes with specified type.
	 * @param {DOMNode} node
	 * @param {Array} nodeTypes
	 */
	getChildNodesOfTypes: function(node, nodeTypes)
	{
		var result = [];
		var types = Kekule.ArrayUtils.toArray(nodeTypes);
		for (var i = 0, l = node.childNodes.length; i < l; ++i)
		{
			var child = node.childNodes[i];
			if (!nodeTypes || (types.indexOf(child.nodeType) >= 0))
				result.push(child);
		}
		return result;
	},
	/**
	 * Get first element child of elem.
	 * @param {Object} elem
	 * @param {String} tagName
	 * @param {String} localName
	 * @param {String} namespaceURI
	 * @returns {Array} Direct element children of elem.
	 */
	getFirstChildElem: function(elem, tagName, localName, namespaceURI)
	{
		for (var i = 0, l = elem.childNodes.length; i < l; ++i)
		{
			var node = elem.childNodes[i];
			if (node.nodeType === Node.ELEMENT_NODE)
			{
				if ((tagName && (tagName.toLowerCase() === node.tagName.toLowerCase())) ||
					(localName && (localName.toLowerCase() === Kekule.DomUtils.getLocalName(node).toLowerCase())) ||
					((!tagName) && (!localName)))
				{
					if ((!namespaceURI) || Kekule.DomUtils.namespaceMatched(node, namespaceURI))
						return node;
				}
			}
		}
	},
	/**
	 * Get first level element children of elem.
	 * @param {Object} elem
	 * @param {String} tagName
	 * @param {String} localName
	 * @param {String} namespaceURI
	 * @returns {Array} Direct element children of elem.
	 */
	getDirectChildElems: function(elem, tagName, localName, namespaceURI)
	{
		var result = [];
		for (var i = 0, l = elem.childNodes.length; i < l; ++i)
		{
			var node = elem.childNodes[i];
			if (node.nodeType === Node.ELEMENT_NODE)
			{
				if ((tagName && (tagName.toLowerCase() === node.tagName.toLowerCase())) ||
					(localName && (localName.toLowerCase() === Kekule.DomUtils.getLocalName(node).toLowerCase())) ||
					((!tagName) && (!localName)))
				{
					if ((!namespaceURI) || Kekule.DomUtils.namespaceMatched(node, namespaceURI))
						result.push(node);
				}
			}
		}
		return result;
	},
	/**
	 * Get first level element children of elem with given attrib values
	 * @param {Object} elem
	 * @param {Array} attribValues Array of hash ({attrib: value}).
	 * @param {Object} tagName
	 * @param {Object} localName
	 * @param {Object} namespaceURI
	 */
	getDirectChildElemsOfAttribValues: function(elem, attribValues, tagName, localName, namespaceURI)
	{
		var elems = Kekule.DomUtils.getDirectChildElems(elem, tagName, localName, namespaceURI);
		var result = [];
		for (var i = 0, l = elems.length; i < l; ++i)
		{
			var fitAttribValue = true;
			for (var j = 0, k = attribValues.length; j < k; ++j)
			{
				var attribName = Kekule.ObjUtils.getOwnedFieldNames(attribValues[j])[0];
				if (elems[i].getAttribute(attribName) != attribValues[j][attribName])
				{
					fitAttribValue = false;
					break;
				}
			}
			if (fitAttribValue)
				result.push(elems[i]);
		}
		return result;
	},
	/**
	 * Check if childElem is inside parentElem.
	 * @param {Object} childElem
	 * @param {Object} parentElem
	 * @returns {Bool}
	 */
	isDescendantOf: function(childElem, parentElem)
	{
		try
		{
			if (childElem && parentElem)
			{
				if (childElem.documentElement)  // no one can be the root of a document
					return false;
				if (childElem.ownerDocument === parentElem)   // parent is document
					return true;

				var parent = childElem.parentNode;
				while (parent)
				{
					if (parent === parentElem)
						return true;
					parent = parent.parentNode;
				}
			}
			return false;
		}
		catch(e)
		{
			return false;
		}
	},
	/**
	 * Check if childElem is inside parentElem or is parentElem itself.
	 * @param {Object} childElem
	 * @param {Object} parentElem
	 * @returns {Bool}
	 */
	isOrIsDescendantOf: function(childElem, parentElem)
	{
		return (childElem === parentElem) || Kekule.DomUtils.isDescendantOf(childElem, parentElem);
	},
	/**
	 * Get nearest ancestor element with specified tag name.
	 * @param {Object} elem
	 * @param {String} tagName
	 * @param {Bool} includingSelf If true, elem will also be check if it is in tagName
	 * @returns {Object}
	 */
	getNearestAncestorByTagName: function(elem, tagName, includingSelf)
	{
		var tag = tagName.toLowerCase();
		if (includingSelf && elem.tagName && (elem.tagName.toLowerCase() === tag))
			return elem;
		var result = elem.parentNode;
		while (result && result.tagName)
		{
			if (result.tagName.toLowerCase() === tag)
				return result;
			result = result.parentNode;
		}
		return null;
	},
	/**
	 * Get value of attribute with the same namespace of elem.
	 * @param {Object} elem
	 * @param {String} attribName
	 * @returns {String} Value or null.
	 * @private
	 */
	getSameNSAttributeValue: function(elem, attribName, domHelper)
	{
		/*
		 var namespaceURI = elem.namespaceURI;
		 var result = elem.getAttribute(attribName);
		 if ((!result) && namespaceURI)
		 {
		 if (elem.getAttributeNS)
		 result = elem.getAttributeNS(namespaceURI, attribName);
		 else if (domHelper)
		 result = domHelper.getAttributeNS(namespaceURI, attribName, elem);
		 }
		 */
		var result = elem.getAttribute(attribName);  // XML attrib are generally not in any namespace
		return result;
	},
	/**
	 * Set value of attribute with the same namespace of elem.
	 * @param {Object} elem
	 * @param {String} attribName
	 * @returns {String} Value or null.
	 * @private
	 */
	setSameNSAttributeValue: function(elem, attribName, value, domHelper)
	{
		/*
		 var namespaceURI = elem.namespaceURI;
		 //var result = elem.getAttribute(attribName);
		 if (namespaceURI)
		 {
		 if (elem.setAttributeNS)
		 result = elem.setAttributeNS(namespaceURI, attribName, value);
		 else if (domHelper)
		 result = domHelper.setAttributeNS(namespaceURI, attribName, value, elem);
		 }
		 else
		 */
		var result = elem.setAttribute(attribName, value);  // XML attrib are generally not in any namespace
		return result;
	},
	/**
	 * Fetch all attribute values into an array
	 * @param {Object} elem
	 * @param {String} namespaceURI
	 * @param {Boolean} useLocalName. Whether fetch attribute's localName rather than name.
	 * @returns {Array} Each item is a hash: {name: '', value: ''}.
	 */
	fetchAttributeValuesToArray: function(elem, namespaceURI, useLocalName)
	{
		var result = [];
		for (var i = 0, l = elem.attributes.length; i < l; ++i)
		{
			var attrib = elem.attributes[i];
			if (namespaceURI && (!Kekule.DomUtils.namespaceMatched(attrib, namespaceURI, true)))
				continue;
			var name;
			if (useLocalName)
			{
				try { name = Kekule.DomUtils.getLocalName(attrib); } catch(e) {name = attrib.name; }
			}
			else
				name = attrib.name;
			result.push({'name': name, 'value': attrib.value});
		}
		return result;
	},
	/**
	 * Fetch all attribute values into an JSON object
	 * @param {Object} elem
	 * @param {String} namespaceURI
	 * @param {Boolean} useLocalName. Whether fetch attribute's localName rather than name.
	 * @returns {Hash} Each item is a hash: {name: value}.
	 */
	fetchAttributeValuesToJson: function(elem, namespaceURI, useLocalName)
	{
		var result = {};
		for (var i = 0, l = elem.attributes.length; i < l; ++i)
		{
			var attrib = elem.attributes[i];
			if (namespaceURI && (!Kekule.DomUtils.namespaceMatched(attrib, namespaceURI, true)))
				continue;
			var name;
			if (useLocalName)
			{
				try { name = Kekule.DomUtils.getLocalName(attrib); } catch(e) {name = attrib.name; }
			}
			else
				name = attrib.name;
			result[name] = attrib.value;
		}
		return result;
	},
	/**
	 * Check if elem has an attribute.
	 * @param {Element} elem
	 * @param {String} attribName
	 * @returns {Bool}
	 */
	hasAttribute: function(elem, attribName)
	{
		if (elem.hasAttribute)
			return elem.hasAttribute(attribName);
		else
		{
			var value = elem.getAttribute(attribName);
			//return Kekule.ObjUtils.notUnset(value);
			return !!value;
		}
	},
	/**
	 * Returns if a name starts with 'data-'.
	 * @param {String} attribName
	 * @returns {Bool}
	 */
	isDataAttribName: function(attribName)
	{
		return attribName.toString().startsWith('data-');
	},
	/**
	 * Returns attribName without 'data-' prefix.
	 * If attribName is not a data attribute, null will be returned.
	 * @param {String} attribName
	 * @returns {String}
	 */
	getDataAttribCoreName: function(attribName)
	{
		if (Kekule.DomUtils.isDataAttribName(attribName))
			return attribName.substr(5);
		else
			return null;
	},
	/**
	 * Get value of HTML5 data-* attribute.
	 * @param {Object} elem
	 * @param {String} attribName
	 * @returns {String}
	 */
	getDataAttrib: function(elem, attribName)
	{
		if (elem.dataset)  // HTML5 way
			return elem.dataset[attribName];
		else  // DOM way
			return elem.getAttribute('data-' + attribName.toString().hyphenize());
	},
	/**
	 * Set value of HTML5 data-* attribute.
	 * @param {Object} elem
	 * @param {String} attribName
	 * @param {String} value
	 */
	setDataAttrib: function(elem, attribName, value)
	{
		if (elem.dataset)  // HTML5 way
			elem.dataset[attribName] = value;
		else  // DOM way
			elem.setAttribute('data-' + attribName.toString().hyphenize(), value);
	},
	/**
	 * Fetch whole HTML dataset of elem.
	 * @param {Object} elem
	 * @returns {Hash}
	 */
	getDataset: function(elem)
	{
		if (elem.dataset)  // HTML5 way
			return elem.dataset;
		else
		{
			var result = {};
			var attribs = elem.attributes;
			for (var i = 0, l = attribs.length; i < l; ++i)
			{
				var attrib = attribs[i];
				var name = attrib.name;
				if (name.toLowerCase().indexOf('data-') === 0)
				{
					var prop = name.substring(5).camelize();
					result[prop] = attrib.value;
				}
			}
			return result;
		}
	},
	/**
	 * Clear child element/text node but reserves attribute nodes.
	 * @param {Element} parentElem
	 */
	clearChildContent: function(parentElem)
	{
		var children = Kekule.ArrayUtils.toArray(parentElem.childNodes);
		for (var i = children.length - 1; i >= 0; --i)
		{
			var node = children[i];
			if (node.nodeType === Node.ATTRIBUTE_NODE)
				continue;

			parentElem.removeChild(node);
		}
		return parentElem;
	},

	/**
	 * Replace tag name of element.
	 * This method actually create a new element and replace the old one.
	 * @param {HTMLElement} elem
	 * @param {String} newTagName
	 * @returns {HTMLElement} New element created.
	 */
	replaceTagName: function(elem, newTagName)
	{
		if (elem.tagName.toLowerCase() !== newTagName.toLowerCase())
		{
			var newElem = elem.ownerDocument.createElement(newTagName);
			// clone attribs
			var attribs = Kekule.DomUtils.fetchAttributeValuesToArray(elem);
			for (var i = 0, l = attribs.length; i < l; ++i)
			{
				newElem.setAttribute(attribs[i].name, attribs[i].value);
			}
			// move children
			var children = Kekule.DomUtils.getDirectChildElems(elem);
			for (var i = 0, l = children.length; i < l; ++i)
			{
				newElem.appendChild(children[i]);
			}
			// change DOM tree
			var parent = elem.parentNode;
			var sibling = elem.nextSibling;
			parent.removeChild(elem);
			if (sibling)
				parent.insertBefore(newElem, sibling);
			else
				parent.appendChild(newElem);
			return newElem;
		}
		else
			return elem;
	},

	/**
	 * Set elem's attributes by values appointed by hash object.
	 * @param {Element} elem
	 * @param {Hash} hash
	 */
	setElemAttributes: function(elem, hash)
	{
		var props = Kekule.ObjUtils.getOwnedFieldNames(hash);
		for (var i = 0, l = props.length; i < l; ++i)
		{
			var prop = props[i];
			var value = hash[prop];
			if (prop && value)
				elem.setAttribute(prop, value);
		}
		return elem;
	},

	/**
	 * Check if node has been inserted to DOM tree of document.
	 * @param {DOMNode} node
	 * @param {Document} doc
	 * @returns {Bool}
	 */
	isInDomTree: function(node, doc)
	{
		if (!node)
			return false;
		if (!doc)
			doc = node.ownerDocument;
		if (doc)
		{
			var docElem = doc.documentElement;
			return Kekule.DomUtils.isDescendantOf(node, docElem) || node === docElem;
		}
		else
			return false;
	}
};

/**
 * Util methods about CSS and style values.
 * @object
 */
Kekule.StyleUtils = {
	/**
	 * Remove a property from inline style.
	 * @param {Object} style Inline style object if element.
	 * @param {String} propName
	 */
	removeStyleProperty: function(style, propName)
	{
		if (style.removeProperty)
			style.removeProperty(propName);
		else if (style.removeAttribute)
			style.removeAttribute(propName);
		else
			style[propName] = null;
	},
	/**
	 * Split a units value to {value, units} hash.
	 * @param {String} value
	 * @returns {Hash}
	 */
	analysisUnitsValue: function(value)
	{
		var r = {};
		r.total = value;
		r.value = parseFloat(value);
		var sunit;
		if (value && value.length && (r.value !== undefined) && (r.value !== NaN))
		{
			sunit = '';
			for (var i = value.length - 1; i >= 0; --i)
			{
				var c = value.charAt(i);
				var isDigital = (c >= '0') && (c <= '9');
				if ((!isDigital) && (c !== '.') && (c !== '-'))
					sunit = c + sunit;
			}
		}
		r.units = sunit || '';
		return r;
	},
	/**
	 * Multiple a units string value.
	 * @param {String} value
	 * @param {Num} times
	 * @returns {String}
	 */
	multiplyUnitsValue: function(value, times)
	{
		var v = Kekule.StyleUtils.analysisUnitsValue(value);
		v.value *= times;
		return '' + v.value + v.units;
	},
	/**
	 * Turn a #RRGGBB or #RGB style string to a integer value.
	 * @param {String} str
	 */
	colorStrToValue: function(str)
	{
		var isLongFormat = str.length > 4;
		var sR = isLongFormat? str.substr(1, 2): str.substr(1, 1);
		var sG = isLongFormat? str.substr(3, 2): str.substr(2, 1);
		var sB = isLongFormat? str.substr(5, 2): str.substr(3, 1);
		if (!isLongFormat)
		{
			sR += sR;
			sG += sG;
			sB += sB;
		}
		var result = (parseInt(sR, 16) << 16) + (parseInt(sG, 16) << 8) + parseInt(sB, 16);
		return result;
	},
	/**
	 * Returns computed style of element. If propName not set, all computed result will be returned.
	 * @param {Object} elem
	 * @param {String} propName
	 * @returns {Variant}
	 */
	getComputedStyle: function(elem, propName)
	{
		var styles;
		var doc = elem.ownerDocument;
		if (!doc)
			return null;
		var view = doc.defaultView;
		if (view && view.getComputedStyle)
		{
			styles = view.getComputedStyle(elem, null);
		}
		else if (elem.currentStyle)  // IE
		{
			styles = elem.currentStyle;
		}

		if (styles)  // some times IE can not fetch currentStyle
			return propName? styles[propName]: styles;
		else
		{
			return null;
		}
	},

	/**
	 * Check if element is likely to be visible on page (only display and visibility style are checked).
	 * @param {HTMLElement} elem
	 * @returns {Bool}
	 */
	isShown: function(elem)
	{
		var U = Kekule.StyleUtils;
		return (U.isDisplayed(elem) && U.isVisible(elem));
	},

	/**
	 * Check if element's CSS display property is not set to 'none'.
	 * @param {HTMLElement} elem
	 * @return {Bool}
	 */
	isDisplayed: function(elem)
	{
		return (Kekule.StyleUtils.getComputedStyle(elem, 'display') || '').toLowerCase() !== 'none';
	},
	/**
	 * Get display style of element.
	 * @param {HTMLElement} elem
	 * @returns (String}
	 */
	getDisplayed: function(elem)
	{
		return Kekule.StyleUtils.getComputedStyle(elem, 'display');
	},
	/**
	 * Set display style of element.
	 * @param {HTMLElement} elem
	 * @param (Variant} value If value is a string, the string will be set to display style.
	 *   If it is a boolean, display style will be set to 'none'/'' on false/true.
	 */
	setDisplay: function(elem, value)
	{
		if (elem)
		{
			if (typeof(value) === 'string')
				elem.style.display = value;
			else
				elem.style.display = (!!value) ? '' : 'none';
		}
	},

	/**
	 * Check if element's CSS visibility property is not set to 'hidden'.
	 * @param {HTMLElement} elem
	 * @return {Bool}
	 */
	isVisible: function(elem)
	{
		return (Kekule.StyleUtils.getComputedStyle(elem, 'visibility') || '').toLowerCase() !== 'hidden';
	},
	/**
	 * Get visibility style of element.
	 * @param {HTMLElement} elem
	 * @returns (String}
	 */
	getVisibility: function(elem)
	{
		return Kekule.StyleUtils.getComputedStyle(elem, 'visibility');
	},
	/**
	 * Set visibility style of element.
	 * @param {HTMLElement} elem
	 * @param (Variant} value If value is a string, the string will be set to visibility style.
	 *   If it is a boolean, visibility style will be set to 'hidden'/'' on false/true.
	 */
	setVisibility: function(elem, value)
	{
		if (elem)
		{
			if (typeof(value) === 'string')
				elem.style.visibility = value;
			else
				elem.style.visibility = (!!value) ? '' : 'hidden';
		}
	},

	/**
	 * Returns whether the element generates a block element box.
	 * @param {HTMLElement} elem
	 * @returns {Bool}
	 */
	isBlockElem: function(elem)
	{
		var display = Kekule.StyleUtils.getComputedStyle(elem, 'display');
		return ['block', 'list-item', 'table', 'flex', 'grid'].indexOf(display) >= 0;
	},

	/**
	 * Check if an element is set with absolute or fixed position style.
	 * @param {HTMLElement} elem
	 * @returns {Bool}
	 */
	isAbsOrFixPositioned: function(elem)
	{
		var position = Kekule.StyleUtils.getComputedStyle(elem, 'position') || '';
		position = position.toLowerCase();
		return (position === 'absolute') || (position === 'fixed');
	},
	/** @private */
	_fillAbsOrFixedPositionStyleStack: function(elem, stack)
	{
		var position = Kekule.StyleUtils.getComputedStyle(elem, 'position') || '';
		position = position.toLowerCase();
		if ((position === 'absolute') || (position === 'fixed'))
		{
			stack.push(position.toLocaleLowerCase());
		}
		var parent = elem.parentNode;
		if (parent && parent.ownerDocument)
			Kekule.StyleUtils._fillAbsOrFixedPositionStyleStack(parent, stack);
		return stack;
	},
	/**
	 * Check the ancestors of elem, if one is set to absolute or fixed position,
	 * returns its position style.
	 * @param {HTMLElement} elem
	 * @returns {Bool}
	 */
	isAncestorPositionFixed: function(elem)
	{
		var parent = elem.parentNode;
		if (parent)
		{
			return Kekule.StyleUtils.isSelfOrAncestorPositionFixed(elem);
		}
		else
			return false;
	},
	/**
	 * Check the elem and its ancestor, if one is set to fixed position, result is true.
	 * @param {HTMLElement} elem
	 * @returns {Bool}
	 */
	isSelfOrAncestorPositionFixed: function(elem)
	{
		var positionStack = [];
		Kekule.StyleUtils._fillAbsOrFixedPositionStyleStack(elem, positionStack);
		if (!positionStack.length)
			return null;
		for (var i = positionStack.length - 1; i >= 0; --i)
		{
			var p = positionStack[i];
			if (p === 'fixed')
				return true;
		}
		return false;
	},

	/**
	 * Set cursor style of an element.
	 * @param {HTMLElement} elem
	 * @param {Variant} cursor A string CSS cursor value or an array of cursor keywords.
	 *   If this param is an array, the first cursor keywords available in current browser will actually be used.
	 * @returns {String} The actually used cursor value.
	 */
	setCursor: function(elem, cursor)
	{
		if (DataType.isArrayValue(cursor))
		{
			for (var i = 0, l = cursor.length; i < l; ++i)
			{
				var currCursor = cursor[i];
				elem.style.cursor = currCursor;
				if (elem.style.cursor === currCursor)  // successfully setted
					return currCursor;
			}
			return elem.style.cursor;
		}
		else // normal string
		{
			elem.style.cursor = cursor;
			return elem.style.cursor;
		}
	},

	/** @private */
	_cssTransformValuesToMatrix: function(cssTransformValues)
	{
		var matrix = Kekule.MatrixUtils.create(3, 3, 0);
		matrix[0][0] = cssTransformValues.a;
		matrix[1][0] = cssTransformValues.b;
		matrix[0][1] = cssTransformValues.c;
		matrix[1][1] = cssTransformValues.d;
		matrix[0][2] = cssTransformValues.tx;
		matrix[1][2] = cssTransformValues.ty;
		return matrix;
	},
	/** @private */
	_matrixToCssTransformValues: function(matrix)
	{
		var result = {
			'a': matrix[0][0],
			'b': matrix[1][0],
			'c': matrix[0][1],
			'd': matrix[1][1],
			'tx': matrix[0][2],
			'ty': matrix[1][2]
		};
		return result;
	},

	/**
	 * Check if an element has a CSS transform.
	 * @param {HTMLElement} elem
	 * @returns {Bool}
	 */
	hasTransform: function(elem)
	{
		var transform = Kekule.StyleUtils.getComputedStyle(elem, 'transform');
		return transform && (transform !== 'none');
	},
	/**
	 * Returns matrix function values of CSS transform property.
	 * @param {HTMLElement} elem
	 * @returns {Hash} {a, b, c, d, tx, ty}
	 */
	getTransformMatrixValues: function(elem)
	{
		var matrix = Kekule.StyleUtils.getComputedStyle(elem, 'transform') || '';
		var values = matrix.match(/-?[\d\.]+/g);
		if (values)
			return {'a': values[0], 'b': values[1], 'c': values[2], 'd': values[3], 'tx': values[4], 'ty': values[5]};
		else
			return null;
	},
	/**
	 * Set the matrix values of CSS transform.
	 * @param {HTMLElement} elem
	 * @param {Array} values
	 */
	setTransformMatrixArrayValues: function(elem, values)
	{
		if (values)
		{
			var sMatrix = 'matrix(' + values.join(',') + ')';
			elem.style.transform = sMatrix;
		}
	},
	/**
	 * Returns a matrix object that represent the 2D transform of element.
	 * @param {HTMLElement} elem
	 * @returns {Array}
	 */
	getTransformMatrix: function(elem)
	{
		var values = Kekule.StyleUtils.getTransformMatrixValues(elem);
		if (values)
		{
			return Kekule.StyleUtils._cssTransformValuesToMatrix(values);
		}
		else
			return null;
	},
	/**
	 * A transformed element may be nested in another transformed parent.
	 * This function returns all the transform matrixes from parent to child.
	 * @param {HTMLElement} elem
	 * @param {Array}
	 */
	getCascadeTranformMatrixes: function(elem)
	{
		var result = [];
		var currElem = elem;
		while (currElem)
		{
			var m = Kekule.StyleUtils.getTransformMatrix(currElem);
			if (m)
				result.unshift(m);
			currElem = currElem.parentNode;
		}
		return result;
	},
	/**
	 * A transformed element may be nested in another transformed parent.
	 * This function returns product of all those transform matrixes from parent to child.
	 * @param {HTMLElement} elem
	 * @param {Array}
	 */
	getTotalTransformMatrix: function(elem)
	{
		var matrixes = Kekule.StyleUtils.getCascadeTranformMatrixes(elem);
		var result = null;
		// child on left, parent on right
		for (var i = matrixes.length - 1; i >= 0; --i)
		{
			var m = matrixes[i];
			if (!result)
				result = m;
			else
				result = Kekule.MatrixUtils.multiply(result, m);
		}
		return result;
	},
	calcInvertTransformMatrix: function(matrix)
	{
		var transValues = Kekule.StyleUtils._matrixToCssTransformValues(matrix);
		var v = transValues;

		// calc inverted values, algorithm from https://blog.csdn.net/qq_17429661/article/details/51985344
		var det = v.a * v.d - v.b * v.c;
		if (Math.abs(det) < 0.000001)  // Singular Matrix
			return false;  // can not calculate

		var v1 = {
			a: v.d / det,
			b: -v.b / det,
			c: -v.c / det,
			d: v.a / det,
			tx: (v.c * v.ty - v.d * v.tx) / det,
			ty: (v.b * v.tx - v.a * v.ty) / det
		};
		var result = Kekule.StyleUtils._cssTransformValuesToMatrix(v1);

		return result;
	}
};

/**
 * Utils methods for HTML elements.
 * @object
 */
Kekule.HtmlElementUtils = {
	/**
	 * Get classes used by element.
	 * @param {HTMLElement} elem
	 * @returns {Array}
	 */
	getClassNames: function(elem)
	{
		return Kekule.StrUtils.splitTokens(elem.className);
	},
	/**
	 * Check if a class is associate with element.
	 * @param {HTMLElement} elem
	 * @param {String} className
	 * @return {Bool}
	 */
	hasClass: function(elem, className)
	{
		var names = Kekule.HtmlElementUtils.getClassNames(elem);
		return (names.indexOf(className) >= 0);
	},
	/**
	 * Add class name(s) to element.
	 * @param {HTMLElement} elem
	 * @param {Variant} className Can be a simple name, or a series of name separated by space ('name1 name2')
	 * 	or an array of strings.
	 */
	addClass: function(elem, className)
	{
		var U = Kekule.HtmlElementUtils;
		var names = Kekule.ArrayUtils.isArray(className)? className: Kekule.StrUtils.splitTokens(className);
		for (var i = 0, l = names.length; i < l; ++i)
		{
			if (!U.hasClass(elem, names[i]))
				elem.className += ' ' + names[i];
		}
		return U;
	},
	/**
	 * remove class(es) from element.
	 * @param {HTMLElement} elem
	 * @param {Variant} className Can be a simple name, or a series of name separated by space ('name1 name2')
	 * 	or an array of strings.
	 */
	removeClass: function(elem, className)
	{
		var U = Kekule.HtmlElementUtils;
		var removedNames = Kekule.ArrayUtils.isArray(className)? className: Kekule.StrUtils.splitTokens(className);
		var names = U.getClassNames(elem);
		for (var i = names.length; i >= 0; --i)
		{
			var index = removedNames.indexOf(names[i]);
			if (index >= 0)
				names.splice(i, 1);
		}
		elem.className = names.join(' ');
		return U;
	},
	/**
	 * Toggle class(es) from element.
	 * @param {HTMLElement} elem
	 * @param {Variant} className Can be a simple name, or a series of name separated by space ('name1 name2')
	 * 	or an array of strings.
	 */
	toggleClass: function(elem, className)
	{
		var U = Kekule.HtmlElementUtils;
		var names = Kekule.ArrayUtils.isArray(className)? className: Kekule.StrUtils.splitTokens(className);
		for (var i = 0, l = names.length; i < l; ++i)
		{
			if (U.hasClass(elem, names[i]))
				U.removeClass(elem, names[i]);
			else
				U.addClass(elem, names[i]);
		}
		return U;
	},

	/**
	 * Returns text (without tag) inside an element.
	 * @param {HTMLElement} elem
	 * @returns {String}
	 */
	getInnerText: function(elem)
	{
		/*
		if (elem.innerText)  // IE
			return elem.innerText;
		else
		{
			var children = elem.children;
			for (var i = 0, l = children.length; i < l; ++i)
			{
				var node = children[i];
				if (node.nodeType === Node.textContent)
			}
		}
		*/
		return elem.innerText || elem.textContent;
	},

	/**
	 * Resize a HTML element.
	 * @param {HTMLElement} elem
	 * @param {Number} width
	 * @param {Number} height
	 * @param {String} unit Default is px.
	 */
	resizeElem: function(elem, width, height, unit)
	{
		elem.style.width = width + (unit || 'px');
		elem.style.height = height + (unit || 'px');
	},
	/**
	 * Get element client width and height.
	 * @param {HTMLElement} elem
	 * @returns {Hash} A combination of {width, height}, in px.
	 */
	getElemClientDimension: function(elem)
	{
		return {'width': elem.clientWidth, 'height': elem.clientHeight};
	},
	/**
	 * Get element scroll width and height.
	 * @param {HTMLElement} elem
	 * @returns {Hash} A combination of {width, height}, in px.
	 */
	getElemScrollDimension: function(elem)
	{
		return {'width': elem.scrollWidth, 'height': elem.scrollHeight};
	},
	/**
	 * Returns computed width/height in px.
	 * @param {HTMLElement} elem
	 * @returns {Hash} A combination of {width, height}, in px.
	 */
	getElemComputedDimension: function(elem)
	{
		var SU = Kekule.StyleUtils;
		var dim = {'width': SU.getComputedStyle(elem, 'width'), 'height': SU.getComputedStyle(elem, 'height')};
		var wInfo = SU.analysisUnitsValue(dim.width);
		var hInfo = SU.analysisUnitsValue(dim.height);
		var result = {};
		if (wInfo.units === 'px')
			result.width = wInfo.value;
		if (hInfo.units === 'px')
			result.height = wInfo.value;
		return result;
	},
	/**
	 * Get element offset width and height.
	 * @param {HTMLElement} elem
	 * @returns {Hash} A combination of {width, height}, in px.
	 */
	getElemOffsetDimension: function(elem)
	{
		return {'width': elem.offsetWidth, 'height': elem.offsetHeight};
	},

	/**
	 * Get size of view port.
	 * @param {Variant} elemOrDocOrViewport
	 * @returns {Hash}
	 */
	getViewportDimension: function(elemOrDocOrViewport)
	{
		// Use the specified window or the current window if no argument
		var w;
		if (elemOrDocOrViewport)
		{
			if (elemOrDocOrViewport.ownerDocument)
				w = Kekule.DocumentUtils.getDefaultView(elemOrDocOrViewport.ownerDocument);
			else if (elemOrDocOrViewport.defaultView)
				w = elemOrDocOrViewport.defaultView;
			else if (elemOrDocOrViewport.parentWindow)
				w = elemOrDocOrViewport.parentWindow;
			else
				w = elemOrDocOrViewport;
		}
		w = w || window;

		// This works for all browsers except IE8 and before
		if (w.innerWidth != null) return { 'width': w.innerWidth, 'height': w.innerHeight };

		// For IE (or any browser) in Standards mode
		var d = w.document;
		if (d.compatMode == "CSS1Compat")
			return { 'width': d.documentElement.clientWidth,
				'height': d.documentElement.clientHeight };

		// For browsers in Quirks mode
		return { 'width': d.body.clientWidth, 'height': d.body.clientHeight };
	},

	/**
	 * Returns coord of top-left and bottom-right of visible viewport part in browser window.
	 * @param {Variant} elemOrDocOrViewport
	 * @returns {Hash}
	 */
	getViewportVisibleBox: function(elemOrDocOrViewport)
	{
		// Use the specified window or the current window if no argument
		var w;
		if (elemOrDocOrViewport)
		{
			if (elemOrDocOrViewport.ownerDocument)
				w = Kekule.DocumentUtils.getDefaultView(elemOrDocOrViewport.ownerDocument);
			else if (elemOrDocOrViewport.defaultView)
				w = elemOrDocOrViewport.defaultView;
			else if (elemOrDocOrViewport.parentWindow)
				w = elemOrDocOrViewport.parentWindow;
			else
				w = elemOrDocOrViewport;
		}
		w = w || window;
		var doc = w.document;

		var dim = Kekule.HtmlElementUtils.getViewportDimension(w);
		var offset = Kekule.DocumentUtils.getScrollPosition(doc);

		var result = {'x1': offset.left, 'y1': offset.top, 'x2': dim.width, 'y2': dim.height};
		result.left = result.x1;
		result.top = result.y1;
		result.right = result.x2;
		result.bottom = result.y2;
		return result;
	},

	/**
	 * Returns bounding client rectangle for element.
	 * @param {HTMLElement} elem
	 * @param {Bool} includeScroll If this value is true, scrollTop/Left of documentElement will be added to result.
	 * @returns {Hash} {top, left, bottom, right, width, height}
	 */
	getElemBoundingClientRect: function(elem, includeScroll)
	{
		var r = Object.extend({}, elem.getBoundingClientRect());
		if (Kekule.ObjUtils.isUnset(r.width))
			r.width = r.right - r.left;
		if (Kekule.ObjUtils.isUnset(r.height))
			r.height = r.bottom - r.top;

		if (includeScroll)
		{
			var doc = elem.ownerDocument;
			var scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop || 0;
			var scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft || 0;
			r.left += scrollLeft;
			r.right += scrollLeft;
			r.top += scrollTop;
			r.bottom += scrollTop;
		}

		//var result = {'left': r.left, 'top': r.top, 'width': r.width, 'height': r.height};
		var result = r;
		result.x = result.left;
		result.y = result.top;
		return result;
	},
	/**
	 * Get position relative to top-left corner of HTML page or current viewport (if includeDocScroll is true).
	 * @param {HTMLElement} elem
	 * @param {Bool} relToViewport
	 * @returns {Hash}
	 */
	getElemPagePos: function(elem, relToViewport)
	{

		var xPosition = 0;
		var yPosition = 0;

		/*
		if (elem.getBoundingClientRect)
		{
			var box = elem.getBoundingClientRect();
			var doc = elem.ownerDocument;
			var body = doc.body;
			var docElem = doc.documentElement;
			var clientTop = docElem.clientTop || body.clientTop || 0;
			var clientLeft = docElem.clientLeft || body.clientLeft || 0;
			yPosition = box.top  + (window && window.pageYOffset || docElem && docElem.scrollTop  || body.scrollTop ) - clientTop,
			xPosition = box.left + (window && window.pageXOffset || docElem && docElem.scrollLeft || body.scrollLeft) - clientLeft;
		}
		else
		*/
		{
			var currElem = elem;
			while(currElem)
			{
				// TODO: Here Chrome report body.scrollLeft unavailable in strict mode warning
				xPosition += (currElem.offsetLeft - currElem.scrollLeft + currElem.clientLeft);
				yPosition += (currElem.offsetTop - currElem.scrollTop + currElem.clientTop);
				currElem = currElem.offsetParent;
			}
			if (relToViewport)
			{
				var doc = elem.ownerDocument;
				var scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop || 0;
				var scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft || 0;
				xPosition -= scrollLeft;
				yPosition -= scrollTop;
			}
		}
		return { x: xPosition, y: yPosition };
	},
	/**
	 * Get position relative to top-left corner of HTML page togather with width/height of elem.
	 * @param {HTMLElement} elem
	 * @param {Bool} relToViewport
	 * @returns {Hash} {x, y, top, left, bottom, right, width, height}
	 */
	getElemPageRect: function(elem, relToViewport)
	{
		var pos = Kekule.HtmlElementUtils.getElemPagePos(elem, relToViewport);
		var dim = Kekule.HtmlElementUtils.getElemClientDimension(elem);
		return {
			'x': pos.x, 'y': pos.y, 'left': pos.x, 'top': pos.y,
			'right': pos.x + dim.width, 'bottom': pos.y + dim.height,
			'width': dim.width, 'height': dim.height
		};
	},
	/**
	 * Get position relative to top-left corner of viewport.
	 * @param {HTMLElement} elem
	 * @returns {Hash}
	 */
	getElemViewportPos: function(elem)
	{
		//var rect = Kekule.HtmlElementUtils.getElemBoundingClientRect(elem);
		var rect = Kekule.HtmlElementUtils.getElemPagePos(elem, true);
		return {'x': rect.left, 'y': left.top};
	},

	/**
	 * Set element's position style to make it to a absolute, relative or fixed one (but retain element's position).
	 * @param {HTMLElement} elem
	 */
	makePositioned: function(elem)
	{
		var p = Kekule.StyleUtils.getComputedStyle(elem, 'position');
		if (!p || p.toLowerCase() === 'static')
			elem.style.position = 'relative';
	},

	/**
	 * Check if element is a form control (input, button, select and textarea).
	 * @param elem
	 */
	isFormCtrlElement: function(elem)
	{
		var formCtrlTags = ['input', 'button', 'textarea', 'select'];
		var tagName = elem && elem.tagName.toLowerCase();
		return !!tagName && formCtrlTags.indexOf(tagName) >= 0;
	},

	/**
	 * Issues an asynchronous request to make the element be displayed full-screen.
	 * @param {HTMLElement} elem
	 */
	requestFullScreen: function(elem)
	{
		var func = elem.requestFullScreen || elem.mozRequestFullScreen || elem.webkitRequestFullScreen || elem.msRequestFullScreen;
		return func? func.apply(elem): null;
	},
	/**
	 * Issues an asynchronous request to make the element exit full-screen mode.
	 * @param {HTMLElement} elem
	 */
	exitFullScreen: function(elem)
	{
		var func = elem.exitFullScreen || elem.mozExitFullScreen || elem.webkitExitFullScreen || elem.msExitFullScreen;
		return func? func.apply(elem): null;
	}
};


/**
 * Utils on document.
 * @object
 */
Kekule.DocumentUtils = {
	/**
	 * Returns the default view (window) of current document.
	 * @param {HTMLDocument} document
	 * @returns {Object}
	 */
	getDefaultView: function(document)
	{
		return document.defaultView || document.parentWindow;
	},
	/**
	 * Returns scroll top/left of document element.
	 * @param {HTMLDocument} document
	 * @returns {Hash} {left, top}
	 */
	getScrollPosition: function(document)
	{
		var win = Kekule.DocumentUtils.getDefaultView(document);
		var result = {
			'left': ((win.pageXOffset !== undefined)?
					win.pageXOffset:
					(document.documentElement || document.body.parentNode || document.body).scrollLeft) || 0,
			'top': ((win.pageYOffset !== undefined)?
					win.pageYOffset:
					(document.documentElement || document.body.parentNode || document.body).scrollTop) || 0
		};
		result.x = result.left;
		result.y = result.top;
		return result;
	},
	/**
	 * Returns dimension of viewport visible client.
	 * @param {HTMLDocument} document
	 * @returns {Hash} {width, height}
	 */
	getClientDimension: function(document)
	{
		if (document.compatMode == "BackCompat")
		{
			return {
				'width': document.body.clientWidth,
				'height': document.body.clientHeight
			}
		}
		else
		{
			return	{
				'width': document.documentElement.clientWidth,
				'height': document.documentElement.clientHeight
			}
		}
	},
	/**
	 * Returns top-left and bottom-right coords and width/height of viewport visible client.
	 * @param {HTMLDocument} document
	 * @returns {Hash} {left, top, right, bottom, x1, x2, y1, y2, width, height}
	 */
	getClientVisibleBox: function(document)
	{
		var offset = Kekule.DocumentUtils.getScrollPosition(document);
		var dim = Kekule.DocumentUtils.getInnerClientDimension(document);
		var result = {};
		result.left = result.x1 = offset.left;
		result.top = result.y1 = offset.top;
		result.right = result.x2 = dim.width + offset.left;
		result.bottom = result.y2 = dim.height + offset.top;
		result.width = dim.width;
		result.height = dim.height;
		return result;
	},
	/**
	 * Returns innerWidth and innerHeight property of a window.
	 * @param {HTMLDocument} document
	 * @returns {Hash} {width, height}
	 */
	getWindowInnerDimension: function(document)
	{
		var win = Kekule.DocumentUtils.getDefaultView(document);
		return {'width': Math.min(win.innerWidth, win.outerWidth), 'height': Math.min(win.innerHeight, win.outerHeight)};
	},
	/**
	 * Returns the visible viewport dimension (on mobile device) or the dimension of whole viewport client (ondesktop).
	 * @param {HTMLDocument} document
	 * @returns {Hash} {width, height}
	 */
	getInnerClientDimension: function(document)
	{
		var clientDim = Kekule.DocumentUtils.getClientDimension(document);
		var innerDim = Kekule.DocumentUtils.getWindowInnerDimension(document);
		return {
			'width': innerDim.width? Math.min(innerDim.width, clientDim.width): clientDim.width,
			'height': innerDim.height? Math.min(innerDim.height, clientDim.height): clientDim.height
		};
	},

	/**
	 * Returns the scale level of current page in mobile browser.
	 * @param {HTMLDocument} document
	 * @returns {Float}
	 */
	getClientScaleLevel: function(document)
	{
		return (document.compatMode == "BackCompat")?
			document.body.clientWidth / window.innerWidth:
			document.documentElement.clientWidth / window.innerWidth;
	},
	/**
	 * Returns the ratio of actual device pixel to CSS 1px.
	 * @param {HTMLDocument} document
	 * @returns {Number}
	 */
	getPixelZoomLevel: function(document)
	{
		var scale = Kekule.DocumentUtils.getClientScaleLevel(document);
		return scale * (Kekule.DocumentUtils.getDefaultView(document).devicePixelRatio || 1);
	},

	/**
	 * Returns PPI of current device.
	 * The algorithm is from https://jsfiddle.net/pgLo6273/2/.
	 * @param {HTMLDocument} document
	 * @returns {Number}
	 */
	getDevicePPI: function(document)
	{
		var win = Kekule.DocumentUtils.getDefaultView(document);
		if (win.matchMedia)
		{
			var minRes = 0;
			var maxRes = 0;
			var curRes = 200;
			var trc = [];
			while (!maxRes || ((maxRes - minRes) > 1))
			{
				if (win.matchMedia('(min-resolution: ' + curRes + 'dpi)').matches)
				{ // ppi >= curRes
					if (maxRes)
					{
						minRes = curRes;
						curRes = Math.round((minRes + maxRes) / 2);
					}
					else
					{
						minRes = curRes;
						curRes = 2 * curRes;
					}
				}
				else
				{ // ppi < curRes
					maxRes = curRes;
					curRes = Math.round((minRes + maxRes) / 2);
				}
			}
			return curRes;
		}
		else
			return 96;  // old device, assume to be a fixed one
	}
};


/**
 * Utils to handle 'url(XXXX)' type of attribute value.
 * it may has three format
 *   content directly
 *   url(http://...)
 *   url(#id)
 * @object
 */
Kekule.DataSrcAttribUtils = {
	/**
	 * Check if str starts with 'url('.
	 * @param {String} str
	 * @returns {Bool}
	 */
	isUrlBasedValue: function(str)
	{
		var p = str.toLowerCase().indexOf('url(');
		return (p === 0);
	},
	/**
	 * Get string value inside 'url()' parenthesis
	 * @return {String}
	 */
	getUrl: function(str)
	{
		var result = str.trim();
		var p = result.toLowerCase().indexOf('url(');
		if (p === 0)
		{
			var p2 = result.lastIndexOf(')');
			result = result.substring(4, p2);
			return result;
		}
		else
			return null;
	},
	/**
	 * Get inside content of element.
	 * @private
	 */
	getElementContent: function(elem)
	{
		if (!elem)
			return null;
		var content = elem.innerHTML;
		// innerHTML often start with a blank line, erase it
		// eliminate the first blank line
		/*
		 var p = content.indexOf('\n');
		 if (p === 0)
		 content = content.substring(1);
		 */
		content = content.ltrim();
		return content;
	},
	/**
	 * Get mimeType mark of elem.
	 * @private
	 */
	getElementMimeType: function(elem)
	{
		return elem.getAttribute('type');
	}
	/*
	 * Fetch actual value of url containing attribute value.
	 * @param {String} str
	 * @param {Object} doc Which document to search for element with ID.
	 * @param {Func} callback As sometimes an AJAX call need to be used to retrieve content of an URL, the result will be returned by this callback.
	 *   The callback has one param: callback(content, mimeType). If retrieving failed, content will be null.
	 * @deprecated
	 */
	/*
	fetchContent: function(str, doc, callback)
	{
		if (!doc)
			doc = document;
		var U = Kekule.DataSrcAttribUtils;
		if (U.isUrlBasedValue(str))
		{
			var url = U.getUrl(str);
			if (url)
			{
				url = url.trim();
				// check if start with '#'
				if (url.charAt(0) === '#')  // follows an id
				{
					var id = url.substring(1);
					var elem = doc.getElementById(id);
					callback(U.getElementContent(elem), U.getElementMimeType(elem));
				}
				else  // a url, need to retrieve content by AJAX
				{
					// TODO: AJAX code is unfinished
				}
			}
			else
				callback(null);
		}
		else
			callback(str);
	}
	*/
};

/**
 * Utils to handle JavaScript file.
 * @object
 */
Kekule.ScriptFileUtils = {
	/** @private */
	_existedScriptUrls: new Kekule.MapEx(),
	/**
	 * Append script file to document. When the script is loaded, callback is then called.
	 * @param {HTMLDocument} doc
	 * @param {String} url
	 * @param {Func} callback
	 * @returns {HTMLElement} New created script element.
	 */
	appendScriptFile: function(doc, url, callback)
	{
		var exists = Kekule.ScriptFileUtils._existedScriptUrls.get(doc);
		if (!exists)
		{
			exists = [];
			Kekule.ScriptFileUtils._existedScriptUrls.set(doc, exists);
		}
		if (exists.indexOf(url) >= 0)  // already loaded
		{
			if (callback)
				callback();
			return;
		}
		var result = doc.createElement('script');
		result.src = url;
		result.onload = result.onreadystatechange = function(e)
		{
			if (result._loaded)
				return;
			var readyState = result.readyState;
			if (readyState === undefined || readyState === 'loaded' || readyState === 'complete')
			{
				result._loaded = true;
				result.onload = result.onreadystatechange = null;
				exists.push(url);
				if (callback)
					callback();
			}
		};
		(doc.getElementsByTagName('head')[0] || doc.body).appendChild(result);
		//console.log('load script', url);
		return result;
	},
	/**
	 * Append script files to document. When the all scripts are loaded, callback is then called.
	 * @param {HTMLDocument} doc
	 * @param {String} urls
	 * @param {Func} callback
	 */
	appendScriptFiles: function(doc, urls, callback)
	{
		var dupUrls = [].concat(urls);

		var _appendScriptFilesCore = function(doc, urls, callback)
		{
			if (urls.length <= 0)
			{
				if (callback)
					callback();
				return;
			}
			var file = urls.shift();
			Kekule.ScriptFileUtils.appendScriptFile(doc, file, function()
				{
					Kekule.ScriptFileUtils.appendScriptFiles(doc, urls, callback);
				}
			);
		};

		_appendScriptFilesCore(doc, dupUrls, callback);
	}
};


})();