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 <table> 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;