@qooxdoo/framework
Version:
The JS Framework for Coders
294 lines (249 loc) • 9.3 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2009 Sebastian Werner, http://sebastian-werner.net
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
======================================================================
This class contains code based on the following work:
* jQuery
http://jquery.com
Version 1.3.1
Copyright:
2009 John Resig
License:
MIT: http://www.opensource.org/licenses/mit-license.php
************************************************************************ */
/**
* This class is mainly a convenience wrapper for DOM elements to
* qooxdoo's event system.
*
* @ignore(qxWeb)
*/
qx.Bootstrap.define("qx.bom.Html",
{
/*
*****************************************************************************
STATICS
*****************************************************************************
*/
statics :
{
/**
* Helper method for XHTML replacement.
*
* @param all {String} Complete string
* @param front {String} Front of the match
* @param tag {String} Tag name
* @return {String} XHTML corrected tag
*/
__fixNonDirectlyClosableHelper : function(all, front, tag)
{
return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
all : front + "></" + tag + ">";
},
/** @type {Map} Contains wrap fragments for specific HTML matches */
__convertMap :
{
opt : [ 1, "<select multiple='multiple'>", "</select>" ], // option or optgroup
leg : [ 1, "<fieldset>", "</fieldset>" ],
table : [ 1, "<table>", "</table>" ],
tr : [ 2, "<table><tbody>", "</tbody></table>" ],
td : [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
col : [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
def : qx.core.Environment.select("engine.name",
{
"mshtml" : [ 1, "div<div>", "</div>" ],
"default" : null
})
},
/**
* Fixes "XHTML"-style tags in all browsers.
* Replaces tags which are not allowed to be closed directly such as
* <code>div</code> or <code>p</code>. They are patched to use opening and
* closing tags instead, e.g. <code><p></code> => <code><p></p></code>
*
* @param html {String} HTML to fix
* @return {String} Fixed HTML
*/
fixEmptyTags : function(html)
{
return html.replace(/(<(\w+)[^>]*?)\/>/g, this.__fixNonDirectlyClosableHelper);
},
/**
* Translates a HTML string into an array of elements.
*
* @param html {String} HTML string
* @param context {Document} Context document in which (helper) elements should be created
* @return {Array} List of resulting elements
*/
__convertHtmlString : function(html, context)
{
var div = context.createElement("div");
html = qx.bom.Html.fixEmptyTags(html);
// Trim whitespace, otherwise indexOf won't work as expected
var tags = html.replace(/^\s+/, "").substring(0, 5).toLowerCase();
// Auto-wrap content into required DOM structure
var wrap, map = this.__convertMap;
if (!tags.indexOf("<opt")) {
wrap = map.opt;
} else if (!tags.indexOf("<leg")) {
wrap = map.leg;
} else if (tags.match(/^<(thead|tbody|tfoot|colg|cap)/)) {
wrap = map.table;
} else if (!tags.indexOf("<tr")) {
wrap = map.tr;
} else if (!tags.indexOf("<td") || !tags.indexOf("<th")) {
wrap = map.td;
} else if (!tags.indexOf("<col")) {
wrap = map.col;
} else {
wrap = map.def;
}
// Omit string concat when no wrapping is needed
if (wrap)
{
// Go to html and back, then peel off extra wrappers
div.innerHTML = wrap[1] + html + wrap[2];
// Move to the right depth
var depth = wrap[0];
while (depth--) {
div = div.lastChild;
}
}
else
{
div.innerHTML = html;
}
// Fix IE specific bugs
if ((qx.core.Environment.get("engine.name") == "mshtml"))
{
// Remove IE's autoinserted <tbody> from table fragments
// String was a <table>, *may* have spurious <tbody>
var hasBody = /<tbody/i.test(html);
// String was a bare <thead> or <tfoot>
var tbody = !tags.indexOf("<table") && !hasBody ?
div.firstChild && div.firstChild.childNodes :
wrap[1] == "<table>" && !hasBody ? div.childNodes :
[];
for (var j=tbody.length-1; j>=0 ; --j)
{
if (tbody[j].tagName.toLowerCase() === "tbody" && !tbody[j].childNodes.length) {
tbody[j].parentNode.removeChild(tbody[j]);
}
}
// IE completely kills leading whitespace when innerHTML is used
if (/^\s/.test(html)) {
div.insertBefore(context.createTextNode(html.match(/^\s*/)[0]), div.firstChild);
}
}
return qx.lang.Array.fromCollection(div.childNodes);
},
/**
* Cleans-up the given HTML and append it to a fragment
*
* When no <code>context</code> is given the global document is used to
* create new DOM elements.
*
* When a <code>fragment</code> is given the nodes are appended to this
* fragment except the script tags. These are returned in a separate Array.
*
* Please note: HTML coming from user input must be validated prior
* to passing it to this method. HTML is temporarily inserted to the DOM
* using <code>innerHTML</code>. As a consequence, scripts included in
* attribute event handlers may be executed.
*
* @param objs {Element[]|String[]} Array of DOM elements or HTML strings
* @param context {Document?document} Context in which the elements should be created
* @param fragment {Element?null} Document fragment to appends elements to
* @return {Element[]} Array of elements (when a fragment is given it only contains script elements)
*/
clean: function(objs, context, fragment)
{
context = context || document;
// !context.createElement fails in IE with an error but returns typeof 'object'
if (typeof context.createElement === "undefined") {
context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
}
// Fast-Path:
// If a single string is passed in and it's a single tag
// just do a createElement and skip the rest
if (!fragment && objs.length === 1 && typeof objs[0] === "string")
{
var match = /^<(\w+)\s*\/?>$/.exec(objs[0]);
if (match) {
return [context.createElement(match[1])];
}
}
// Iterate through items in incoming array
var obj, ret=[];
for (var i=0, l=objs.length; i<l; i++)
{
obj = objs[i];
// Convert HTML string into DOM nodes
if (typeof obj === "string") {
obj = this.__convertHtmlString(obj, context);
}
// Append or merge depending on type
if (obj.nodeType) {
ret.push(obj);
} else if (obj instanceof qx.type.BaseArray ||
(typeof qxWeb !== "undefined" && obj instanceof qxWeb)) {
ret.push.apply(ret, Array.prototype.slice.call(obj, 0));
} else if (obj.toElement) {
ret.push(obj.toElement());
} else {
ret.push.apply(ret, obj);
}
}
// Append to fragment and filter out scripts... or...
if (fragment)
{
return qx.bom.Html.extractScripts(ret, fragment);
}
// Otherwise return the array of all elements
return ret;
},
/**
* Extracts script elements from an element list. Optionally
* attaches them to a given document fragment
*
* @param elements {Element[]} list of elements
* @param fragment {Document?} document fragment
* @return {Element[]} Array containing the script elements
*/
extractScripts : function(elements, fragment) {
var scripts=[], elem;
for (var i=0; elements[i]; i++) {
elem = elements[i];
if (elem.nodeType == 1 && elem.tagName.toLowerCase() === "script" && (!elem.type || elem.type.toLowerCase() === "text/javascript"))
{
// Trying to remove the element from DOM
if (elem.parentNode) {
elem.parentNode.removeChild(elements[i]);
}
// Store in script list
scripts.push(elem);
}
else
{
if (elem.nodeType === 1)
{
// Recursively search for scripts and append them to the list of elements to process
var scriptList = qx.lang.Array.fromCollection(elem.getElementsByTagName("script"));
elements.splice.apply(elements, [i+1, 0].concat(scriptList));
}
// Finally append element to fragment
if (fragment) {
fragment.appendChild(elem);
}
}
}
return scripts;
}
}
});