diff --git a/src/main/webapp/scripts/globals.js b/src/main/webapp/scripts/globals.js
index 5c602a6655670407a1f7d2f204fdfa61b82d41e8..1198212052c90d04ada5101ad3c957d629c4a63b 100644
--- a/src/main/webapp/scripts/globals.js
+++ b/src/main/webapp/scripts/globals.js
@@ -290,16 +290,19 @@ function forEach( arr, fn ){
 // of an object's own properties
 // works like jQuery's $.each()
 // but only for objects
+// returns array of property names
 function forOwn( obj, fn ){
-    var _key;
-    if (!isObject(obj)) { return }
-    if (isFunction(fn)) {
-        for ( _key in obj ){
-            if (obj.hasOwnProperty(_key)) {
-                fn(_key, obj[_key]);
-            }
+    var keys = [],
+        key;
+    if (!isPlainObject(obj)) { return }
+    for (key in obj) {
+        if (obj.hasOwnProperty(key)) {
+            keys.push(key);
+            if (!isFunction(fn)) continue;
+            fn(key, obj[key]);
         }
     }
+    return keys;
 }
 
 // convert array-like object or arguments to a real array
@@ -307,11 +310,11 @@ function forOwn( obj, fn ){
 function toArray(arr) {
     var i = -1,
         len = arr.length,
-        _args = new Array(len);
+        newArray = new Array(len);
     while (++i < len) {
-        _args[i] = arr[i];
+        newArray[i] = arr[i];
     }
-    return _args;
+    return newArray;
 }
 
 // check if 'item' is in 'arr' array
@@ -595,20 +598,24 @@ function encodeURIComponentAll(str) {
 }
 
 function setElementData(element, name, val){
-    if (document.head && document.head.dataset){
+    if (document.head && document.head.dataset) {
+        name = toCamelCase(name);
         element.dataset[name] = val;
     }
     else {
-        element.setAttribute('data-'+name, val);
+        name = toDashed(name);
+        element.setAttribute('data-' + name, val);
     }
 }
 
 function getElementData(element, name){
-    if (document.head && document.head.dataset){
-        return element.dataset[name];
+    if (document.head && document.head.dataset) {
+        name = toCamelCase(name);
+        return realValue(element.dataset[name]);
     }
     else {
-        return element.getAttribute('data-'+name);
+        name = toDashed(name);
+        return realValue(element.getAttribute('data-' + name));
     }
 }
 
@@ -620,6 +627,8 @@ function getElementData(element, name){
 // a string used in [data-] attributes
 function realValue(val, bool){
     var undefined;
+    // only evaluate strings
+    if (!isString(val)) return val;
     if (bool){
         if (val === '0'){
             return false;
@@ -845,37 +854,37 @@ function insertScript( url, min, name ){
 }
 
 // insertScripts([{url:'/scripts/app/script',name:'app.script',min:'-min'}]);
-function insertScripts( /* scripts (multiple args or array) */ ){
-    var i = -1, scripts;
-    if (isString(arguments[0]) || arguments.length > 1){
-        scripts = toArray(arguments);
-    }
-    else {
-        scripts = arguments[0];
-    }
-    while (++i < scripts.length){
-        if (scripts[i]){ // skip null values
-            insertScript(scripts[i]);
-        }
-    }
-}
-insertScripts.configArraySample = [
-    // string with pipe separating params (spaces ok)
-    // (script url) | (optional min string) | (optional script name)
-    '/scripts/app/foo.js | .min | foo',
-    // or use an object with param properties
-    {
-        url: '/scripts/app/script', // REQUIRED
-        min: '-min', // optional
-        name: 'app.script' // optional
-    },
-    {
-        // 'src' property name works also
-        src: '/scripts/app/utils',
-        min: '.min',
-        name: 'app.utils'
-    }
-];
+//function insertScripts( /* scripts (multiple args or array) */ ){
+//    var i = -1, scripts;
+//    if (isString(arguments[0]) || arguments.length > 1){
+//        scripts = toArray(arguments);
+//    }
+//    else {
+//        scripts = arguments[0];
+//    }
+//    while (++i < scripts.length){
+//        if (scripts[i]){ // skip null values
+//            insertScript(scripts[i]);
+//        }
+//    }
+//}
+//insertScripts.configArraySample = [
+//    // string with pipe separating params (spaces ok)
+//    // (script url) | (optional min string) | (optional script name)
+//    '/scripts/app/foo.js | .min | foo',
+//    // or use an object with param properties
+//    {
+//        url: '/scripts/app/script', // REQUIRED
+//        min: '-min', // optional
+//        name: 'app.script' // optional
+//    },
+//    {
+//        // 'src' property name works also
+//        src: '/scripts/app/utils',
+//        min: '.min',
+//        name: 'app.utils'
+//    }
+//];
 
 // returns new <script> DOM ELEMENT
 function scriptElement( src, title, body ){
diff --git a/src/main/webapp/scripts/lib/jquery-plugins/jquery.spawn.js b/src/main/webapp/scripts/lib/jquery-plugins/jquery.spawn.js
old mode 100644
new mode 100755
index 96278ef735a85314b00cc0062693bdaec691f141..b692ecfba06f100e3612def2e953701eb50f1d7f
--- a/src/main/webapp/scripts/lib/jquery-plugins/jquery.spawn.js
+++ b/src/main/webapp/scripts/lib/jquery-plugins/jquery.spawn.js
@@ -36,11 +36,11 @@ if (typeof jQuery == 'undefined') {
     // var $div2 = $.spawn('div'), {}, {id:'div2'}, "Div2's HTML content")
     /**
      * Create a jQuery-wrapped DOM object
-     * @param {String} tag - HTML tag
-     * @param {Object} opts - jQuery methods / child element(s) / HTML
-     * @param {Object} attr - native DOM methods, properties, and attributes
-     * @param {String|Array|Object|Elements} content - child element(s) / HTML
-     * @returns {object} - jQuery object
+     * @param tag {String} HTML tag name
+     * @param [opts] {Object|String} jQuery methods / child element(s) / HTML
+     * @param [attr] {Object} native DOM methods, properties, and attributes
+     * @param [content] {String|Array|Object|Element} child element(s) / HTML
+     * @returns {*|HTMLElement}
      */
     $.spawn = function(tag, opts, attr, content){
 
@@ -63,6 +63,11 @@ if (typeof jQuery == 'undefined') {
             tag = firstDefined(opts.tag||undefined, '');
         }
 
+        // 'tag' could be an array of child content in recursive spawns
+        if ($.isArray(tag)){
+            _opts.children = tag;
+        }
+
         // trim outer white space and remove any trailing semicolons or commas
         parts = tag.trim().replace(/(;|,)$/,'').split('|');
 
@@ -94,7 +99,7 @@ if (typeof jQuery == 'undefined') {
 
         attrs = (parts[1]||'').split(/;|,/) || []; // allow ';' or ',' for attribute delimeter
 
-        attrs.forEach(function(att){
+        $.each(attrs, function(i, att){
             if (!att) return;
             var sep = /:|=/; // allow ':' or '=' for key/value separator
             var quotes = /^('|")|('|")$/g;
@@ -113,17 +118,22 @@ if (typeof jQuery == 'undefined') {
             if ($.isPlainObject(attr)){
                 // pull out the 'prop' properties
                 if (attr.prop){
-                    $el.prop(attr.prop);
+                    $el.prop.apply($el, [].concat(attr.prop));
                     delete attr.prop;
                 }
+                $.each(attr, function(name, prop){
+                    el[name] = prop;
+                });
             }
-            try {
-                // could be an object map of multiple attributes
-                // or could be an array for a single attribute - ['name','foo']
-                $el.attr.apply($el, [].concat(attr));
-            }
-            catch(e){
-                if (console && console.log) console.log(e);
+            else {
+                try {
+                    // could be an object map of multiple attributes
+                    // or could be an array for a single attribute - ['name','foo']
+                    $el.attr.apply($el, [].concat(attr));
+                }
+                catch(e){
+                    if (console && console.log) console.log(e);
+                }
             }
         }
 
@@ -134,23 +144,31 @@ if (typeof jQuery == 'undefined') {
         opts = opts || {};
 
         // just append an HTML string, jQuery object, element, or fragment
-        if (typeof opts == 'string' || opts.jquery || isElement(opts) || isFragment(opts)){
-            return $el.append(opts);
+        if (typeof opts == 'string' || typeof opts == 'function' || opts.jquery || isElement(opts) || isFragment(opts)){
+            content = opts;
+            //return $el.append(opts);
         }
-
         // if 'opts' is an array, they
         // will be child elements
-        if ($.isArray(opts)) {
+        else if ($.isArray(opts)) {
             _opts.children = opts;
         }
-
         // otherwise it's a config object
-        if ($.isPlainObject(opts)) {
+        else if ($.isPlainObject(opts)) {
             _opts = $.extend(true, {}, opts);
         }
 
         // a fourth argument can contain additional content
         if (content){
+            if (typeof content == 'function'){
+                try {
+                    content = content();
+                }
+                catch(e){
+                    if (console && console.log) console.log(e);
+                    content = [];
+                }
+            }
             _opts.content = [].concat(_opts.content||[], content);
         }
 
@@ -179,7 +197,7 @@ if (typeof jQuery == 'undefined') {
             // to the new element
             // (without jQuery)
             if (/^(element|el)$/.test(prop)){
-                $.each(val, function(name, value){
+                $.each([].concat(val), function(name, value){
                     el[name] = value;
                 });
                 return;
@@ -189,7 +207,11 @@ if (typeof jQuery == 'undefined') {
             // an array of elements
             // to be spawned
             if (/^(children|content|contents)$/.test(prop)) {
-                $.each([].concat(val), function(i, child){
+                val = [].concat(val);
+                if (val.length === 1){
+                    return $el.append(val);
+                }
+                $.each(val, function(i, child){
                     try {
                         // recursively append spawns as needed
                         //$el.append(child); // each child must be an 'appendable' item
@@ -236,4 +258,79 @@ if (typeof jQuery == 'undefined') {
 
     };
 
+    /**
+     * Leaner and faster jQuery element spawner
+     * @param tag {String|Object} tag name or jQuery object
+     * @param [$opts] {Object|String} jQuery options or 'appendable' content
+     * @param [opts] {Object|String} element options or 'appendable' content
+     * @param [content] {String|Element} 'appendable' content
+     * @returns {*|HTMLElement}
+     */
+    $.spawn.element = function(tag, $opts, opts, content){
+
+        var el, $el, argLen = arguments.length;
+
+        if (argLen === 0){
+            return $(document.createDocumentFragment());
+        }
+
+        // 'tag' arg is required but can be either
+        // a string for the tag name or a jQuery object
+        el = tag.jquery ? tag[0] : document.createElement(tag);
+        $el = $(el);
+
+        if (argLen === 1){
+            return $el;
+        }
+        else if (argLen === 2){
+            if (/(string|number)/.test(typeof $opts)){
+                el.innerHTML += $opts+'';
+                return $el;
+            }
+        }
+        else if (argLen === 3){
+            if (/(string|number)/.test(typeof opts)){
+                el.innerHTML += opts+'';
+                content = null;
+                opts = null;
+            }
+        }
+
+        if (content){
+            $el.append([].concat(content));
+        }
+
+        if (opts){
+            forOwn(opts, function(prop, val){
+                el[prop] = val;
+            });
+        }
+
+        if ($opts){
+            forOwn($opts, function(prop, val){
+                $el[prop].apply($el, [].concat(val));
+            });
+            //forOwn($opts||{}, function(prop, val){
+            //    $el[prop] = val;
+            //});
+        }
+
+        return $el;
+
+    };
+
+    $.spawn.trial = $.spawn.time = function(tag, count){
+        tag = tag || 'div';
+        count = count || 1000;
+        var i = -1,
+            time = Date.now(),
+            frag$ = $.spawn();
+        while (++i < count){
+            frag$.append($.spawn.apply(null, [].concat(tag)));
+        }
+        console.log('time: ' + ((Date.now() - time) / 1000 ) + 's');
+        return frag$;
+    };
+
+
 })(jQuery);
diff --git a/src/main/webapp/scripts/lib/spawn/spawn.html b/src/main/webapp/scripts/lib/spawn/spawn.html
index 7554bb6715410a71ed4a328e313b514bf5c7af06..76cbfe742d73b0916b3fbb2604ed4bb0ac3d483d 100644
--- a/src/main/webapp/scripts/lib/spawn/spawn.html
+++ b/src/main/webapp/scripts/lib/spawn/spawn.html
@@ -3,9 +3,17 @@
 <head>
     <meta charset="UTF-8">
     <title>Spawn Samples</title>
+    <style>
+        .block { display: block; }
+        .margin20 { margin: 20px; }
+        .pad20 { padding: 20px; }
+    </style>
     <script src="../jquery/jquery.js"></script>
     <script src="../jquery-plugins/jquery.spawn.js"></script>
     <script src="spawn.js"></script>
+    <!-- the 'lite' script overrides the 'lite' method in spawn.js -->
+    <script src="../jquery-plugins/jquery.spawn.alt.js"></script>
+    <script src="../../utils.js"></script>
     <script>
         // shortcuts
         function byId(id){
@@ -17,6 +25,9 @@
 
 <h1>Samples for spawn.js</h1>
 
+<h2>spawn.plus() example</h2>
+<div id="spawn-lite"></div>
+
 <h2>Spawn a simple &lt;table&gt; element:</h2>
 <div id="spawn-table-1"></div>
 
@@ -27,19 +38,43 @@
 <p>(click a table cell to trigger an 'onclick' event)</p>
 <div id="spawn-table-3"></div>
 
-<h2>Spawn elements with the jQuery Spawner <b>$.spawn()</b>:</h2>
+<h2>Spawn elements with the jQuery Spawner <b>$.spawn.lite()</b>:</h2>
 <p>The jQuery Spawner returns a jQuery-wrapped element.</p>
+<p><i>(here's a grab bag of $.spawn()ed elements)</i></p>
 <div id="jquery-spawn-div"></div>
 
+<!-- output
+
+<div id="spawn1">
+    <h1 title="TITLE!" style="padding: 10px; background: rgb(224, 224, 224);">Bar</h1>
+    Foo.
+    <br>
+    <input>
+    <br>
+    <label class="pad20 block">
+        <input type="checkbox" checked="checked">CHECKBOX
+    </label>
+    <br class="clear">
+    <button title="Blah" style="font-size: 20px;">Foo!</button>
+    <hr style="border: 1px solid red;">
+    <ul>
+        <li class="foo"><i>FOO!!!</i></li>
+        <li class="bar"><b>BAR!!!</b></li>
+    </ul>
+</div>
+
+
+-->
+
 <script>
-    (function(){
+    (function($){
 
         function th(opts, content){
             if (!content) {
                 content = opts;
                 opts = {};
             }
-            return spawn('td', opts, content);
+            return spawn.plus('td', opts, content);
         }
 
         function td(opts, content){
@@ -47,7 +82,7 @@
                 content = opts;
                 opts = {};
             }
-            return spawn('td', opts, content);
+            return spawn.plus('td', opts, content);
         }
 
         function tr(opts, cells){
@@ -56,7 +91,7 @@
                 cells = opts;
                 opts = {};
             }
-            return spawn('tr', opts, cells.map(function(content){
+            return spawn.plus('tr', opts, cells.map(function(content){
                 return td(content);
             }));
 //            var _row = spawn('tr', opts, cells.map(function(content){
@@ -75,9 +110,11 @@
                 ['x', 'y', 'z'],
                 [1, 2, 3]
             ];
+
+            opts.className = 'foo';
             // -OR- an array of objects using the keys for <th> elements
             // (also add a [data-key="key_name"] attribute)
-            var _table = spawn('table', opts);
+            var _table = spawn.plus('table', opts);
             if (rows) {
                 rows.forEach(function(row){
                     _table.appendChild(tr(null, row));
@@ -103,7 +140,43 @@
             return tr({ className: classes }, data);
         }
 
-        byId('spawn-table-1').appendChild(spawn('table|id=table1', [
+        function div(opts){
+            return spawn.plus('div', opts)
+        }
+
+        window.spawnLite = function(){
+            var _spawnLite = byId('spawn-lite');
+            _spawnLite.innerHTML = '';
+            _spawnLite.appendChild(spawn.plus('div', {
+                innerHTML: 'Foo ',
+                title: '(foo)',
+                onclick: function(){
+                    alert(this.title)
+                },
+                append: document.createElement('hr'),
+                fn: [
+                    {appendChild: spawn.plus('b', 'Bar!')},
+                    {appendChild: spawn.plus('br')},
+                    {appendChild: spawn.plus('i', 'Baz.')}
+                ]
+            }));
+            return _spawnLite;
+        };
+
+        window.spawnLite();
+
+        byId('spawn-table-1').appendChild(spawn.plus('table', {
+            id: 'table1',
+            fn: {
+                // use 'appendChild' to add elements LAST
+                appendChild: spawn.plus('tr', [
+                    ['td', 'Bonus 1'],
+                    ['td', 'Bonus 2'],
+                    ['td', 'Bonus 3'],
+                    ['td', 'Bonus 4']
+                ])
+            }
+        }, [
             ['tr', [
                 td('Cell 1'),
                 td('Cell 2'),
@@ -125,12 +198,68 @@
             alert(this.innerHTML);
         }));
 
-        $('#jquery-spawn-div').append($.spawn('div|id=spawn1', [
-            ['', 'Foo'],
-            ['h1', 'Bar']
+        // $.spawn('div', {jQuery}, {Element}, 'children/text/html');
+
+        $('#jquery-spawn-div').append($.spawn.lite('div|id=spawn1', {
+            on: {
+                // bind 'click' event to <h1> elements
+                click: ['h1', function(){
+                    alert(this.innerHTML)
+                }]
+            }
+        }, null, [
+            ['h1|title=TITLE!', {
+                html: 'BAR',
+                css: {
+                    padding: '20px',
+                    background: '#f0f0f0'
+                }
+            }, {
+                onclick: function(){
+                    alert(this.title)
+                }
+            }],
+            'Foo. ',
+            ['br'],
+            ['input', null, {
+                value: 'abc',
+                //prop: ['disabled', 'true'],
+                onblur: function(){
+                    this.value = !this.value ? 'abc' : this.value;
+                },
+                onfocus: function(){
+                    this.value = '';
+                }
+            }],
+            ['br'],
+            ['label|class=pad20 block', null, [
+                ['input|type=checkbox;checked'],
+                'CHECKBOX'
+            ]],
+            ['br|class=clear'],
+            ['button', {
+                css: ['font-size', '20px'],
+                html: 'Foo! ',
+                on: {
+                    click: function(){
+                        alert(this.innerHTML);
+                    },
+                    mouseover: function(){
+                        console.log(this.title)
+                    }
+                }
+            }, {
+                data: { foo: 'bar' },
+                title: 'Blah'
+            }],
+            ['hr', {css:['border','1px solid red']}],
+            ['ul', null, [
+                ['li|class=foo', null, '<i>FOO!!!</i>'],
+                ['li|class=bar', null, '<b>BAR!!!</b>']
+            ]]
         ]));
 
-    }())
+    })(jQuery)
 </script>
 
 </body>
diff --git a/src/main/webapp/scripts/lib/spawn/spawn.js b/src/main/webapp/scripts/lib/spawn/spawn.js
index 27b4fa0d4de95909099c3a69a9db87680bb0a7ee..fc402ca87e106ee6c2186e81e9276e7dad8f00c9 100644
--- a/src/main/webapp/scripts/lib/spawn/spawn.js
+++ b/src/main/webapp/scripts/lib/spawn/spawn.js
@@ -10,71 +10,127 @@
 
 (function(window, doc){
 
-    var undefined;
-
-    function isElement(it){
-        return it.nodeType && it.nodeType === 1;
-    }
-
-    function isFragment(it){
-        return it.nodeType && it.nodeType === 11;
-    }
-
-    // main factory function
+    var undefined,
+        UNDEFINED = 'undefined';
+
+    // which HTML elements are
+    // self-closing "void" elements?
+    var voidElements = [
+        'area',
+        'base',
+        'br',
+        'col',
+        'command',
+        'embed',
+        'hr',
+        'img',
+        'input',
+        'keygen',
+        'link',
+        'meta',
+        'param',
+        'source',
+        'track',
+        'wbr'
+    ];
+
+    // boolean element attributes
+    var boolAttrs = [
+        'disabled',
+        'selected',
+        'checked',
+        'multiple'
+    ];
+
+    // which "type" values create an <input> element?
+    var inputTypes = [
+        'text',
+        'password',
+        'number',
+        'email',
+        'date',
+        'url',
+        'checkbox',
+        'radio',
+        'hidden'
+    ];
+
+    /**
+     * Full-featured (but slowest) element spawner
+     * @param tag {String|Object} tag name or config object
+     * @param [opts] {Object|String|Array} config object, HTML string or array of 'appendable' items
+     * @param [inner] {String|Array} HTML string or array of 'appendable' items
+     * @returns {Element|*}
+     */
     function spawn(tag, opts, inner){
 
-        var el, parts, attrs, classArray=[], contents='', children,
-            use$, $el, $opts={}, toDelete=['tag', 'tagName'];
+        var el, parts, attrs, use$, $el, children,
+            DIV        = doc.createElement('div'),
+            classArray = [],
+            contents   = '',
+            $opts      = {},
+            toDelete   = ['tag', 'tagName'];
 
-        if (!isDefined(tag)) {
+        if (typeof tag == UNDEFINED) {
             return doc.createDocumentFragment();
         }
 
         // handle cases where 'tag' is already an element
-        if (isElement(tag) || isFragment(tag)){
-            return tag;
-            //el = tag;
-            //tag = el.tagName; // will this create a new element?
+        if (isElement(tag) || isFragment(tag)) {
+            //return tag;
+            el = tag;
+            tag = el.tagName; // will this create a new element?
         }
 
         tag = typeof tag == 'string' ? tag.trim() : tag;
 
-        if (arguments.length === 1){
-            if (Array.isArray(tag)){
+        if (arguments.length === 1) {
+            if (Array.isArray(tag)) {
                 children = tag;
                 tag = '#html';
             }
-            else if (tag === '!'){
+            else if (tag === '!') {
                 return doc.createDocumentFragment();
             }
-            else if (typeof tag == 'string' && tag !== '' &&
-                !(/^(#text|#html|!)|\|/gi.test(tag))
-            ){
-                return doc.createElement(tag||'span')
+            else if (typeof tag == 'string' && tag !== '' && !(/^(#text|#html|!)|\|/gi.test(tag))) {
+                return doc.createElement(tag || 'span')
             }
         }
 
         // make sure opts is defined
-        opts = opts || {};
+        //opts = opts || {};
 
-        if (arguments.length === 3){
+        if (arguments.length === 3) {
             contents = inner;
         }
 
-        if (Array.isArray(opts) || typeof opts == 'string'){
+        if (Array.isArray(opts) || typeof opts == 'string' || typeof opts == 'function') {
             contents = opts;
         }
+        else {
+            opts = opts || {};
+        }
 
-        if (isPlainObject(tag)){
+        if (isPlainObject(tag)) {
             opts = tag;
-            tag = firstDefined(opts.tag||opts.tagName||undefined, '#html');
+            tag = opts.tag || opts.tagName || '#html';
         }
 
         // NOW make sure opts is an Object
         opts = getObject(opts);
 
-        if (typeof contents == 'number'){
-            contents = contents+'';
+        if (typeof contents == 'number') {
+            contents = contents + '';
+        }
+
+        if (typeof contents == 'function'){
+            try {
+                contents = contents();
+            }
+            catch(e){
+                if (console && console.log) console.log(e);
+                contents = [];
+            }
         }
 
         // combine 'content', 'contents', and 'children' respectively
@@ -86,11 +142,11 @@
         // trim outer white space and remove any trailing
         // semicolons or commas from 'tag'
         // (shortcut for adding attributes)
-        parts = tag.trim().replace(/(;|,)$/,'').split('|');
+        parts = tag.trim().replace(/(;|,)$/, '').split('|');
 
         tag = parts[0].trim();
 
-        if (el && (isElement(el) || isFragment(el))){
+        if (el && (isElement(el) || isFragment(el))) {
             // don't do anything if
             // el is already an element
         }
@@ -105,19 +161,19 @@
             //}
             // pass empty string '', '#text', or '#html' as first argument
             // to create a textNode
-            if (tag === '' || /^(#text|#html|!)|\|/gi.test(tag)){
+            if (tag === '' || /^(#text|#html|!)|\|/gi.test(tag)) {
                 el = doc.createDocumentFragment();
                 //el.appendChild(doc.createTextNode(contents));
                 //return el;
             }
             else {
                 try {
-                    el = doc.createElement(tag||'span');
+                    el = doc.createElement(tag || 'span');
                 }
-                catch(e){
+                catch(e) {
                     if (console && console.log) console.log(e);
                     el = doc.createDocumentFragment();
-                    el.appendChild(doc.createTextNode(tag||''));
+                    el.appendChild(doc.createTextNode(tag || ''));
                 }
             }
         }
@@ -127,33 +183,40 @@
         // or (colons for separators, commas for delimeters, no quotes),:
         // spawn('input|type:checkbox,id:foo-ckbx');
         // allow ';' or ',' for attribute delimeter
-        attrs = parts[1] ? parts[1].split(/;|,/) || null : null;
+        attrs = parts[1] ? parts[1].split(/;|,/) || [] : [];
 
         forEach(attrs, function(att){
             if (!att) return;
             var sep = /:|=/; // allow ':' or '=' for key/value separator
             var quotes = /^('|")|('|")$/g;
             var key = att.split(sep)[0].trim();
-            var val = (att.split(sep)[1]||'').trim().replace(quotes, '') || key;
+            var val = (att.split(sep)[1] || '').trim().replace(quotes, '') || key;
             // add each attribute/property directly to DOM element
             //el[key] = val;
             el.setAttribute(key, val);
         });
 
-        // any 'data-' attributes?
-        if (opts.data) {
-            forOwn(opts.data, function(name, val){
-                el.setAttribute('data-'+name, val);
-            });
-        }
-
         // 'attr' property (object) to EXPLICITLY set attribute=value
-        if (opts.attr){
+        opts.attr = opts.attr || opts.attrs || opts.attributes;
+        if (opts.attr) {
             forOwn(opts.attr, function(name, val){
+                // if a 'data' object snuck in 'attr'
+                if (name.data) {
+                    opts.data = name.data;
+                    delete name.data;
+                    return;
+                }
                 el.setAttribute(name, val);
             });
         }
 
+        // any 'data-' attributes?
+        if (opts.data) {
+            forOwn(opts.data, function(name, val){
+                setElementData(el, name, val);
+            });
+        }
+
         toDelete.push('data', 'attr');
 
         //opts = isPlainObject(opts) ? opts : {};
@@ -162,9 +225,9 @@
         // jQuery stuff needs to be in a property named $, jq, jQuery, or jquery
         opts.$ = opts.$ || opts.jq || opts.jQuery || opts.jquery;
 
-        use$ = isDefined(opts.$||undefined);
+        use$ = isDefined(opts.$ || undefined);
 
-        if (use$){
+        if (use$) {
             // copy to new object so we can delete from {opts}
             forOwn(opts.$, function(method, args){
                 $opts[method] = args;
@@ -182,7 +245,7 @@
         toDelete.push('classes', 'classNames', 'addClass');
 
         forEach(opts.className.join(' ').split(/\s+/), function(name){
-            if (classArray.indexOf(name) === -1){
+            if (classArray.indexOf(name) === -1) {
                 classArray.push(name)
             }
         });
@@ -206,7 +269,7 @@
 
         // add remaining properties and attributes to element
         // (there should only be legal attributes left)
-        if (isPlainObject(opts)){
+        if (isPlainObject(opts)) {
             forOwn(opts, function(attr, val){
                 el[attr] = val;
             });
@@ -214,17 +277,21 @@
 
         forEach(contents, function(part){
             try {
-                if (typeof part == 'string'){
-                    el.innerHTML += part;
+                if (typeof part == 'string') {
+                    DIV = doc.createElement('div');
+                    DIV.innerHTML = part;
+                    while (DIV.firstChild){
+                        el.appendChild(DIV.firstChild);
+                    }
                 }
-                else if (isElement(part) || isFragment(part)){
+                else if (isElement(part) || isFragment(part)) {
                     el.appendChild(part);
                 }
                 else {
                     el.appendChild(spawn.apply(null, [].concat(part)))
                 }
             }
-            catch(e){
+            catch(e) {
                 if (console && console.log) console.log(e);
             }
         });
@@ -234,18 +301,17 @@
         // - element or fragment
 
         // OPTIONALLY do some jQuery stuff, if specified (and available)
-        if (use$ && isDefined(window.jQuery||undefined)){
+        if (use$ && isDefined(window.jQuery || undefined)) {
             $el = window.jQuery(el);
             forOwn($opts, function(method, args){
-                method = method.toLowerCase();
                 // accept on/off event handlers with varying
                 // number of arguments
-                if (/^(on|off)$/.test(method)){
+                if (/^(on|off)$/.test(method.toLowerCase())) {
                     forOwn(args, function(evt, fn){
                         try {
                             $el[method].apply($el, [].concat(evt, fn));
                         }
-                        catch(e){
+                        catch(e) {
                             if (console && console.log) console.log(e);
                         }
                     });
@@ -255,24 +321,392 @@
             });
             //return $el;
         }
-        
+
         return el;
 
     }
 
+    /**
+     * Leanest and fastest element spawner
+     * @param tag {String|Object} tag name or config object
+     * @param [opts] {Object|String|Array} config object, HTML content, or array of Elements
+     * @param [content] {String|Array} HTML content or array of Elements
+     * @returns {Element|*}
+     */
+    spawn.element = function(tag, opts, content){
+
+        var el;
+
+        if (typeof tag != 'string'){
+            // if 'tag' isn't a string,
+            // it MUST be a config object
+            opts = tag;
+            // and it MUST have a 'tag'
+            // or 'tagName' property
+            tag = opts.tag || opts.tagName || 'div';
+        }
+
+        el = doc.createElement(tag||'div');
+
+        // return early for basic usage
+        if (!content && !opts && typeof tag == 'string') {
+            return el;
+        }
+
+        // allow use of only 2 arguments
+        // with the HTML text being the second
+        if (/(string|number)/.test(typeof opts)){
+            el.innerHTML += (opts+'');
+            return el;
+        }
+        else if (Array.isArray(opts)){
+            content = opts;
+            opts = {};
+        }
+
+        // add attributes and properties to element
+        forOwn(opts, function(prop, val){
+            if (prop === 'tag') return;
+            el[prop] = val;
+        });
+
+        // add any HTML content or child elements
+        if (content){
+            [].concat(content).forEach(function(item){
+                if (/(string|number)/.test(typeof item)){
+                    el.innerHTML += (item+'');
+                }
+                else {
+                    el.appendChild(item);
+                }
+            });
+        }
+
+        return el;
+
+    };
+    // alias
+    spawn.basic = spawn.element;
+
+    /**
+     * Fairly lean and fast element spawner that's
+     * a little more robust than spawn.element().
+     * @param tag {String} element's tagName
+     * @param [opts] {Object|Array|String} element
+     *        properties/attributes -or- array of
+     *        children -or- HTML string
+     * @param [children] {Array|String}
+     *        array of child element 'spawn' arg arrays
+     *        or elements or HTML string
+     * @returns {Element}
+     */
+    spawn.plus = function(tag, opts, children){
+
+        var el, parts, attrs,
+            skip = [], // properties to skip later
+            errors = []; // collect errors
+
+        parts = tag.split('|');
+
+        tag = parts.shift().trim();
+
+        el = doc.createElement(tag||'div');
+
+        if (parts.length){
+            // pass element attributes in 'tag' string, like:
+            // spawn('a|id="foo-link";href="foo";class="bar"');
+            // or (colons for separators, commas for delimeters, no quotes),:
+            // spawn('input|type:checkbox,id:foo-ckbx');
+            attrs = (parts[0]||'').split(/;|,/) || []; // allow ';' or ',' for attribute delimeter
+            attrs.forEach(function(att, i){
+                if (!att) return;
+                var sep = /:|=/; // allow ':' or '=' for key/value separator
+                var quotes = /^('|")|('|")$/g;
+                var key = att.split(sep)[0].trim();
+                var val = (att.split(sep)[1]||'').trim().replace(quotes, '') || key;
+                // allow use of 'class', but (secretly) use 'className'
+                if (key === 'class') {
+                    el.className = val;
+                    return;
+                }
+                el.setAttribute(key, val);
+            });
+        }
+
+        if (!opts && !children){
+            // return early for
+            // basic element creation
+            return el;
+        }
+
+        opts = opts || {};
+        children = children || null;
+
+        // if 'opts' is a string,
+        // set el's innerHTML and
+        // return the element
+        if (typeof opts == 'string'){
+            el.innerHTML += opts;
+            return el;
+        }
+
+        // if 'children' arg is not present
+        // and 'opts' is really an array
+        if (!children && Array.isArray(opts)){
+            children = opts;
+            opts = {};
+        }
+        // or if 'children' is a string
+        // set THAT to the innerHTML
+        else if (typeof children == 'string'){
+            el.innerHTML += children;
+            children = null;
+        }
+
+        // add innerHTML now, if present
+        el.innerHTML += (opts.innerHTML||opts.html||'');
+
+        // append any spawned children
+        if (children && Array.isArray(children)){
+            children.forEach(function(child){
+                // each 'child' can be an array of
+                // spawn arrays...
+                if (Array.isArray(child)){
+                    el.appendChild(spawn.plus.apply(el, child));
+                }
+                // ...or an HTML string...
+                else if (typeof child == 'string'){
+                    el.innerHTML += child;
+                }
+                // ...or 'appendable' nodes
+                else {
+                    try {
+                        el.appendChild(child);
+                    }
+                    catch(e){
+                        // fail silently
+                        errors.push('Error processing children: ' + e);
+                    }
+                }
+            });
+        }
+
+        // special handling of 'append' property
+        if (opts.append){
+            // a string should be HTML
+            if (typeof opts.append == 'string'){
+                el.innerHTML += opts.append;
+            }
+            // otherwise an 'appendable' node
+            else {
+                try {
+                    el.appendChild(opts.append);
+                }
+                catch(e){
+                    errors.push('Error appending: ' + e);
+                }
+            }
+        }
+
+        // DO NOT ADD THESE DIRECTLY TO 'el'
+        skip.push('innerHTML', 'html', 'append', 'attr', 'data', 'fn');
+
+        // add attributes and properties to element
+        forOwn(opts, function(prop, val){
+            // only add if NOT in 'skip' array
+            if (skip.indexOf(prop) === -1){
+                el[prop] = val;
+            }
+        });
+
+        // explicitly add element attributes
+        if (opts.attr){
+            forOwn(opts.attr, function(name, val){
+                el.setAttribute(name, val);
+            });
+        }
+
+        // explicitly add 'data-' attributes
+        if (opts.data){
+            forOwn(opts.data, function(name, val){
+                setElementData(el, name, val);
+            });
+        }
+
+        // execute element methods last...
+        // attach object or array of methods
+        // to 'fn' property - this can be an
+        // array in case you want to run the
+        // same method(s) more than once
+        if (opts.fn){
+            [].concat(opts.fn).forEach(function(fn){
+                forOwn(fn, function(f, args){
+                    el[f].apply(el, [].concat(args));
+                });
+            });
+        }
+
+        if (errors.length){
+            if (console && console.log) console.log(errors)
+        }
+
+        return el;
+
+    };
+    // aliases
+    spawn.lite = spawn.plus;
+    spawn.alt = spawn.plus;
+
+    /**
+     * Spawn an HTML string using input parameters
+     * Simple but not super fast
+     * @param tag {String} tag name for HTML element
+     * @param [attrs] {Object|Array|String} element attributes
+     * @param [content] {String|Array} string or array of strings for HTML content
+     * @returns {String} HTML string
+     */
+    spawn.html = function(tag, attrs, content){
+        // the 'template' method can be useful
+        // for easily churning out plain old HTML
+        // no event handlers or other methods
+
+        tag = tag || 'div';
+        attrs = attrs || null;
+        content = content || [];
+
+        var output = {};
+        output.inner = '';
+        output.attrs = '';
+
+        // use these as a shortcut to create <input> elements:
+        // spawn.html('input|text')
+        //
+        var inputTags = inputTypes.map(function(type){
+            return 'input|' + type;
+        });
+
+        if (inputTags.indexOf(tag) > -1){
+            tag = tag.split('|');
+            output.attrs += (' type="' + tag[1] +'"');
+            tag = tag[0];
+            // maybe set 'content' as the value?
+            output.attrs += (' value="' + content + '"');
+            // add content to [data-content] attribute?
+            //output.attrs += (' data-content="' + content + '"');
+        }
+
+        var isVoid = voidElements.indexOf(tag) > -1;
+
+        if (inputTypes.indexOf(tag))
+
+        if (isVoid){
+            output.open = '<' + tag;
+            output.close = '>';
+        }
+        else {
+            output.open = '<' + tag;
+            output.inner = '>' + [].concat(content).join(' ');
+            output.close = '</' + tag + '>';
+        }
+
+        // process the attributes;
+        if (attrs){
+            if (isPlainObject(attrs)){
+                forOwn(attrs, function(attr, val){
+                    if (boolAttrs.indexOf(attr) > -1){
+                        if (attr){
+                            // boolean attributes don't need a value
+                            output.attrs += (' ' + attr);
+                        }
+                    }
+                    else {
+                        output.attrs += (' ' + attr + '="' + val + '"');
+                    }
+                });
+            }
+            else {
+                output.attrs += [''].concat(attrs).join(' ');
+            }
+        }
+
+        return output.open + output.attrs + output.inner + output.close;
+
+    };
+
+    // convenience alias
+    spawn.fragment = function(el){
+        var frag = doc.createDocumentFragment();
+        if (el){
+            frag.appendChild(el);
+        }
+        return frag;
+    };
+
+    // test spawning speed
+    spawn.speed = function(tag, count, method){
+        tag = tag || 'div';
+        count = count || 1000;
+        var i = -1,
+            time = Date.now(),
+            span = spawn.element('span'),
+            output = [],
+            fn = method ? spawn[method] : spawn,
+            el;
+        while (++i < count){
+            el = fn.apply(null, [].concat(tag));
+            output.push(el);
+            //if (typeof el == 'string'){
+            //    span.innerHTML += el;
+            //}
+            //else {
+            //    span.appendChild(el);
+            //}
+        }
+        time = ((Date.now() - time)/1000);
+        return {
+            time: time + 's',
+            output: output
+        }
+    };
+
+    // compare performance of different spawn methods
+    spawn.speed.compare = function(tag, count){
+        tag = tag || 'div';
+        count = count || 1000;
+        return {
+            spawn: spawn.speed(tag, count, '').time,
+            element: spawn.speed(tag, count, 'element').time,
+            plus: spawn.speed(tag, count, 'plus').time,
+            html: spawn.speed(tag, count, 'html').time
+        }
+    };
+
     // export to the global window object
     window.spawn = spawn;
 
     //
     // utility functions:
     //
+
+    function isElement(it){
+        return it.nodeType && it.nodeType === 1;
+    }
+
+    function isFragment(it){
+        return it.nodeType && it.nodeType === 11;
+    }
+
     function isDefined(it){
         return typeof it != 'undefined';
     }
 
+    function isNumeric(num){
+        return !Array.isArray(num) && (num - parseFloat(num) + 1) >= 0;
+    }
+
     // returns first defined argument
     // useful for retrieving 'falsey' values
-    function firstDefined() {
+    function firstDefined(){
         var undefined, i = -1;
         while (++i < arguments.length) {
             if (arguments[i] !== undefined) {
@@ -282,7 +716,7 @@
         return undefined;
     }
 
-    function isPlainObject( obj ){
+    function isPlainObject(obj){
         return Object.prototype.toString.call(obj) === '[object Object]';
     }
 
@@ -290,22 +724,100 @@
         return isPlainObject(obj) ? obj : {};
     }
 
-    function forEach( arr, fn ){
+    function forEach(arr, fn){
         if (!arr) return;
         var i = -1, len = arr.length;
-        while (++i < len){
+        while (++i < len) {
             fn(arr[i], i);
         }
     }
 
-    function forOwn( obj, fn ){
+    function forOwn(obj, fn){
         if (!obj) return;
-        var key;
-        for ( key in obj ){
+        var keys = [],
+            key;
+        for (key in obj) {
             if (obj.hasOwnProperty(key)) {
+                keys.push(key);
+                if (typeof fn != 'function') continue;
                 fn(key, obj[key]);
             }
         }
+        return keys;
+    }
+
+    function setElementData(element, name, val){
+        if (document.head && document.head.dataset) {
+            name = camelize(name);
+            element.dataset[name] = val;
+        }
+        else {
+            name = hyphenize(name);
+            element.setAttribute('data-' + name, val);
+        }
+    }
+
+    function getElementData(element, name){
+        if (document.head && document.head.dataset) {
+            name = camelize(name);
+            return realValue(element.dataset[name]);
+        }
+        else {
+            name = hyphenize(name);
+            return realValue(element.getAttribute('data-' + name));
+        }
+    }
+
+    // returns real boolean for boolean string
+    // returns real number for numeric string
+    // returns null and undefined for those strings
+    // (or returns original value if none of those)
+    // useful for pulling 'real' values from
+    // a string used in [data-] attributes
+    function realValue(val, bool){
+        var undefined;
+        // only evaluate strings
+        if (typeof val != 'string') return val;
+        if (bool) {
+            if (val === '0') {
+                return false;
+            }
+            if (val === '1') {
+                return true;
+            }
+        }
+        if (isNumeric(val)) {
+            return +val;
+        }
+        switch(val) {
+            case 'true':
+                return true;
+            case 'false':
+                return false;
+            case 'undefined':
+                return undefined;
+            case 'null':
+                return null;
+            default:
+                return val;
+        }
+    }
+
+    function hyphenize(name){
+        return name.replace(/([A-Z])/g, function(u){
+            return '-' + u.toLowerCase();
+        });
+    }
+
+    // set 'forceLower' === true (or omit argument)
+    // to ensure *only* 'cameled' letters are uppercase
+    function camelize(name, forceLower){
+        if (firstDefined(forceLower, false)) {
+            name = name.toLowerCase();
+        }
+        return name.replace(/\-./g, function(u){
+            return u.substr(1).toUpperCase();
+        });
     }
 
 })(this, document);
\ No newline at end of file
diff --git a/src/main/webapp/scripts/polyfills.js b/src/main/webapp/scripts/polyfills.js
index 7e09f3390ebf9aca55138a0e76ed7ddbf10f473b..20b0a733d9e7dd47166597591be62d15f3d6d4ce 100644
--- a/src/main/webapp/scripts/polyfills.js
+++ b/src/main/webapp/scripts/polyfills.js
@@ -2,6 +2,29 @@
  * Polyfills for older browsers (IE8).
  */
 
+// Shortcut for element.appendChild()
+Element.prototype.append = Element.prototype.appendChild;
+
+// Add an element as a first child of another element
+Element.prototype.prepend = function(childNode){
+    if (this.firstChild) {
+        this.insertBefore(childNode, this.firstChild);
+    }
+    else {
+        this.appendChild(childNode);
+    }
+};
+
+// Source: https://github.com/Alhadis/Snippets/blob/master/js/polyfills/IE8-child-elements.js
+if(!("lastElementChild" in document.documentElement)){
+    Object.defineProperty(Element.prototype, "lastElementChild", {
+        get: function(){
+            for(var nodes = this.children, n, i = nodes.length - 1; i >= 0; --i)
+                if(n = nodes[i], 1 === n.nodeType) return n;
+            return null;
+        }
+    });
+}
 
 // String.trim() polyfill (IE8)
 if (!String.prototype.trim) {
diff --git a/src/main/webapp/scripts/utils.js b/src/main/webapp/scripts/utils.js
index 5aa4465f9f75b903847c9987c86d7f2e76344f02..a7c09742693dd4ae232b34bdf0ccb3010c2d3bb0 100644
--- a/src/main/webapp/scripts/utils.js
+++ b/src/main/webapp/scripts/utils.js
@@ -3,6 +3,64 @@
  * depend on jQuery. (load AFTER jQuery)
  */
 
+/**
+ * Test function timing after iterating [count] number of times
+ * @param fn {Function} REQUIRED - function we're testing
+ * @param [args] {Array} ([]) - arguments to pass to [fn]
+ * @param [count] {Number} (1000) - number of iterations
+ * @param [context] (null) - context for [this]
+ * @param [undefined] {undefined}
+ * @returns {Object|*}
+ */
+function speedTest(fn, args, count, context, undefined){
+
+    var i = -1,
+        result = null,
+        returned = [],
+        start = Date.now(),
+        elapsed;
+
+    if (fn == undefined){
+        return 'Test function is undefined.';
+    }
+
+    function timing(time){
+        var out = {};
+        out.num = out.ms = (Date.now() - time);
+        out.milliseconds = out.ms+'ms';
+        out.sec = (out.ms/1000);
+        out.seconds = (out.sec + 's');
+        return out;
+    }
+
+    count   = count   || 1000;
+    context = context || null;
+
+    // collect any results
+    while(++i < count){
+        try {
+            result = fn.apply(context, [].concat(args||[]));
+        }
+        catch(e){
+            result = e;
+            break;
+        }
+        returned.push(result);
+    }
+
+    if (!returned.length){
+        return result;
+    }
+
+    elapsed = timing(start);
+    elapsed.returned = returned;
+
+    console.log(elapsed.seconds);
+
+    return elapsed;
+
+}
+
 
 // return REST url with common parts pre-defined
 // restUrl('/data/projects', ['format=json'])
diff --git a/src/main/webapp/scripts/xnat/element.js b/src/main/webapp/scripts/xnat/element.js
new file mode 100644
index 0000000000000000000000000000000000000000..2b777b2d2d07a69a8fecb19a71bc0f00f6420233
--- /dev/null
+++ b/src/main/webapp/scripts/xnat/element.js
@@ -0,0 +1,185 @@
+/*!
+ * Methods for generating DOM elements on-the-fly
+ * Uses spawn.js behind the scenes.
+ */
+
+var XNAT = getObject(XNAT||{});
+
+(function(XNAT){
+
+    var element, undefined;
+
+    // tolerate passing 'opts' and 'content'
+    // arguments in reverse order
+    function setOpts(opts, content){
+        // if there is only one argument, 
+        // it could be content OR opts
+        if (!content) {
+            content = opts;
+            opts = {};
+        }
+        // if 'content' is an object, put it first
+        if (isPlainObject(content)){
+            return [content, '']
+        }
+        return [opts, content];
+    }
+
+    function setupElement(tag, opts, content){
+        var setup = setOpts(opts, content);
+        return spawn.element(tag, setup[0], setup[1]);
+    }
+
+    function Element(tag, opts, content){
+        //this.element = this;
+        if (!tag) {
+            this.element = spawn.fragment();
+            this.isFragment = true;
+        }
+        else {
+            this.element = setupElement(tag, opts, content);
+        }
+        this.rootElement = this.element;
+        this.parent = this.element;
+    }
+
+    Element.p = Element.prototype;
+
+    Element.p.content = Element.p.html = function(content){
+        this.lastElement = this.lastElement || this.parent || this.rootElement;
+        this.lastElement.innerHTML = [
+            this.lastElement.innerHTML,
+            content
+        ].join(' ');
+        return this;
+    };
+
+    // return root element and all children
+    Element.p.get = function(){
+        if (this.isFragment){
+            if (this.rootElement.childNodes.length){
+                return this.rootElement.childNodes;
+            }
+        }
+        return this.rootElement;
+    };
+
+    Element.p.get$ = function(){
+        return $(this.get())
+    };
+
+    // return last element in the chain
+    Element.p.getLast = function(){
+        if (this.isFragment){
+            return this.rootElement.lastElementChild || this.rootElement;
+        }
+        return this.lastElement;
+    };
+
+    Element.p.getLast$ = function(){
+        return $(this.getLast())
+    };
+
+    Element.p.upTo = Element.p.up = function(tag){
+        // don't go past the root element
+        if (this.lastElement === this.rootElement){
+            this.parent = this.rootElement;
+            return this;
+        }
+        // go up one right away
+        this.parent = this.lastElement = this.lastElement.parentNode;
+        // return early for simple usage
+        if (!tag) return this;
+        // keep going if 'tag' is specified
+        var parentTag = this.parent.tagName.toLowerCase();
+        tag = tag ? tag.toLowerCase() : parentTag;
+        if (tag !== parentTag){
+            this.upTo(tag);
+        }
+        return this;
+    };
+
+    Element.p.closest = function(selector){
+        this.parent = this.lastElement =
+            $(this.lastElement).closest(selector)[0];
+        return this;
+    };
+
+    // chainable spawner
+    // XNAT.element('div').p()._b('Bold text. ')._i('Italic text.');
+    // -> <div><p><b>Bold text. </b><i>Italic text.</i></p></div>
+    XNAT.element = XNAT.el = element = function(tag, opts, content){
+        return new Element(tag, opts, content);
+    };
+
+    // space-separated list of elements
+    // for auto-generated functions
+    // like:
+    // XNAT.element.div('Foo') -> <div>Foo</div>
+    // XNAT.element.br() -> <br>
+    // full list of Elements:
+    // https://developer.mozilla.org/en-US/docs/Web/HTML/Element
+    var tagNames = ('' +
+    'div span p q a h1 h2 h3 h4 h5 h6 main ' +
+    'header footer nav section hgroup article ' +
+    'table thead tr th tbody td tfoot col colgroup ' +
+    'ul ol li dl dt dd hr br iframe ' +
+    's small sub sup u b i em strong pre ' +
+    'form fieldset button input textarea ' +
+    'select option optgroup ' +
+    'img map area embed object script' +
+    '').split(/\s+/);
+
+    tagNames.forEach(function(tag, i){
+
+        // don't process empty 'tag'
+        if (!tag) return;
+
+        // don't overwrite existing functions
+        if (isFunction(element[tag])) return;
+
+        // add siblings after
+        Element.p['_'+tag] = function(opts, content){
+            var el = setupElement(tag, opts, content);
+            this.parent.appendChild(el);
+            this.lastElement = el;
+            return this;
+        };
+
+        //// add siblings
+        //Element.p['_'+tag] = function(opts, content){
+        //    var el = setupElement(tag, opts, content);
+        //    this.lastParent = this.lastElement.parentNode;
+        //    this.lastParent.appendChild(el);
+        //    this.parent = el;
+        //    this.lastElement = el;
+        //    return this;
+        //};
+
+        // add generators to prototype for chaining
+        Element.p[tag] = function(opts, content){
+            var el = setupElement(tag, opts, content);
+            this.parent.appendChild(el);
+            // set parent to THIS element
+            // for creating child elements
+            this.parent = el;
+            this.lastElement = el;
+            return this;
+        };
+
+        // generate tag functions to call
+        // without calling XNAT.element() first
+        // XNAT.element.div('Foo')
+        // -> <div>Foo</div>
+        element[tag] = function(opts, content){
+            var args = setOpts(opts, content);
+            return spawn.element(tag, args[0], args[1]);
+        }
+
+    });
+
+    // TODO: make element methods chainable:
+    // XNAT.element.div({id:'foo'}).p({className:'bar'}, 'Foo Bar');
+    // --> <div id="foo"><p class="bar">Foo Bar</p></div>
+
+})(XNAT);
diff --git a/src/main/webapp/scripts/xnat/url.js b/src/main/webapp/scripts/xnat/url.js
index ed608a8fd5a99c86dfa837072f020648e949994d..27c8a4d63d21d01e6b4777830a0a6f57552dc18b 100644
--- a/src/main/webapp/scripts/xnat/url.js
+++ b/src/main/webapp/scripts/xnat/url.js
@@ -149,7 +149,7 @@ var XNAT = getObject(XNAT||{});
         else if (isString(query) && query.charAt(0) === '?'){
             // make sure a pesky url hash doesn't sneak in there
             query = query.split('#')[0];
-            qsArray = query.split('?')[1].split('&');
+            qsArray = (query.split('?')[1]||'').split('&');
         }
 
         // add qsArray to qsOutput
@@ -276,7 +276,7 @@ var XNAT = getObject(XNAT||{});
     // return the value of a query string parameter,
     // either from a provided url string or the current
     // page's location if only 1 argument is passed
-    url.getQueryStringValue = url.getParam = function(url, parameter){
+    url.getQueryStringValue = url.getParamValue = function(url, parameter){
         if (arguments.length === 1){
             parameter = url;
             url = window.location.href;
@@ -369,9 +369,7 @@ var XNAT = getObject(XNAT||{});
             }
         }
 
-        url = urlSetup(url);
-
-        return XNAT.url.addQueryString(url, params);
+        return urlSetup(url, '', params);
 
     };
 
@@ -409,7 +407,7 @@ var XNAT = getObject(XNAT||{});
         }
         else {
             // 'parts' must be array, object, or string
-            return newUrl;
+            //return newUrl;
         }
 
         if (base && isString(base)){
@@ -427,7 +425,7 @@ var XNAT = getObject(XNAT||{});
         }
 
         // remove multiple slashes and remove '/' in front of '?'
-        return rootUrl(newUrl).replace(/\/\?/g, '?');
+        return rootUrl(newUrl).replace(/\/+\?/g, '?');
 
     }
     url.buildUrl = url.setup = urlSetup;