UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

807 lines (790 loc) 21.7 kB
/** * @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. * @param {HTMLElement} elem * @param {String} text * @param {Bool} multiline If true, text with multiple lines will automatically insert <br /> tags in elem. */ setElementText: function(elem, text, multiline) { 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 lines = multiline? (text || '').split('\n'): [text || '']; for (var i = 0, l = lines.length; i < l; ++i) { if (i > 0) { var elemBr = elem.ownerDocument.createElement('br'); elem.appendChild(elemBr); } var textNode = elem.ownerDocument.createTextNode(lines[i]); 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; }, /** * Returns the parent node of node. * @param {Node} node * @param {Hash} options Currently the options can be {acrossShadowRoot: bool}. * @returns {Node} */ getParentNode: function(node, options) { var op = options || {}; var result = node.parentNode; if (result) return result; else if (op.acrossShadowRoot && node.host) // host of shadow node return node.host; else return null; }, /** * Check if childElem is inside parentElem. * @param {Object} childElem * @param {Object} parentElem * @param {Hash} options Currently the options can be {acrossShadowRoot: bool}. * @returns {Bool} */ isDescendantOf: function(childElem, parentElem, options) { var op = options || {}; 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 getParent = Kekule.DomUtils.getParentNode; var parent = getParent(childElem, op); // childElem.parentNode; while (parent) { if (parent === parentElem) return true; parent = getParent(parent, op); //parent.parentNode; } } return false; } catch(e) { return false; } }, /** * Check if childElem is inside parentElem or is parentElem itself. * @param {Object} childElem * @param {Object} parentElem * @param {Hash} options Currently the options can be {acrossShadowRoot: bool}. * @returns {Bool} */ isOrIsDescendantOf: function(childElem, parentElem, options) { return (childElem === parentElem) || Kekule.DomUtils.isDescendantOf(childElem, parentElem, options); }, /** * 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 * @param {Hash} options Currently the options can be {acrossShadowRoot: bool}. * @returns {Bool} */ isInDomTree: function(node, doc, options) { var op = options || {}; if (!node) return false; if (!doc) doc = node.ownerDocument; if (doc) { if (!op.acrossShadowRoot && node.getRootNode) { return (node.getRootNode() === doc); } else { var docElem = doc.documentElement; return Kekule.DomUtils.isDescendantOf(node, docElem, options) || node === docElem; } } else return false; }, /** * Check if node is in a shadow root context. * @param {Node} node * @returns {Bool} */ isInShadowRoot: function(node) { if (typeof(ShadowRoot) !== 'undefined') { try { var rootNode = node.getRootNode && node.getRootNode(); return rootNode instanceof ShadowRoot; } catch(e) { return false; } } else return false; } }; /** * 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 Callback(error). If error is null, the loading process is success. * @returns {HTMLElement} New created script element. */ appendScriptFile: function(doc, url, callback) { var exists; if (doc) { 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(null); return; } var result = doc.createElement('script'); result.src = url; result.onerror = function(error){ if (callback) callback(error); }; 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(null); } }; (doc.getElementsByTagName('head')[0] || doc.body).appendChild(result); //console.log('load script', url); return result; } else // doc is null, maybe in node environment { var rawUtil = Kekule.$jsRoot['__$kekule_scriptfile_utils__']; if (rawUtil && rawUtil.appendScriptFile) return rawUtil.appendScriptFile(doc, url, callback); } }, /** * 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, errors) { if (urls.length <= 0) { if (callback) { if (!errors.length) callback(null); else callback(new Error('Error in loading: ' + errors.join('; '))); } return; } var file = urls.shift(); Kekule.ScriptFileUtils.appendScriptFile(doc, file, function(error) { if (error) { errors.push(file + ': ' + error? (error.message || error): ''); } _appendScriptFilesCore(doc, urls, callback, errors); }); }; _appendScriptFilesCore(doc, dupUrls, callback, []); } }; })();