javascript.dElement() source
source code
/** dElement / dAppend requires Array.isArray() requires Array.prototype.filter() requires Array.prototype.forEach() requires Array.prototype.indexOf() requires Array.prototype.some() requires Function.prototype.bind() requires K345.attrNames requires K345.voidElements */ (function (attrNames, voidElems) { ''; 'use strict'; /* internal vars */ var _slice = Array.prototype.slice, dAppend_regex = (/[#\.=\[\]:\s]+/), eventStack, initStack, refs, loopdepth, /* predefined data */ skipProps, saveProps, formProps, boolProps, multiProps, /* functions */ hasOwn, dError, isNode, isEl, isAppendable, isTextNode, parseElemStr, strToNodes; /* ============== COMMON FUNCTIONS ================= */ /** test if object 'obj' has own property 'prop' @param {object} obj Object to test @param {string} prop Property which must be in 'obj' @returns {boolean} true, if 'prop' is a native property of 'obj' @function */ hasOwn = (function () { return (isMeth(Object, 'hasOwn')) /* browsers supporting Object.hasOwn() */ ? function (obj, prop) { return Object.hasOwn(obj, prop); } /* fallback for browsers without Object.hasOwn support */ : function (obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }; })(); /** test if el is a nodeElement and has a specific nodeType @param {HTMLElement} el Element to test @returns {boolean} true, if nodetype matches item in array '@this'. @this {Array} allowed nodeTypes to test against */ function nodeTest (el) { /* "nodeType" in el must NOT be replaced by call to hasOwnProperty! */ return isObj(el) && 'nodeType' in el && this.indexOf(el.nodeType) > -1; } /** test: is 'el' a DOM element? @function @name isEl @param {HTMLElement} el Element to test @returns {boolean} true if 'el' is a nodeElement(1) */ isEl = nodeTest.bind([ Node.ELEMENT_NODE ]); /** test: is 'el' a DOM element or a documentFragment? @function @name isNode @param {HTMLElement} el Element to test @returns {boolean} true if 'el' is a nodeElement(1) or a documentFragment(11) */ isNode = nodeTest.bind([ Node.ELEMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE ]); /** test: can 'el' be appended to nodeElements? @function @name isAppendable @param {HTMLElement} el Element to test @returns {boolean} true if 'el' is a nodeElement(1), a documentFragment(11), a comment(8) or a textNode(3) */ isAppendable = nodeTest.bind([ Node.ELEMENT_NODE, Node.TEXT_NODE, Node.COMMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE ]); /** test: is 'el' a text node? @function @name isTextNode @param {HTMLElement} el Element to test @returns {boolean} true if 'el' is a textNode(3) */ isTextNode = nodeTest.bind([ Node.TEXT_NODE ]); /** remove dash(es) (-) from a string and convert the following char to uppercase ( no-text => noText it-is-fine => itIsFine) @param {string} str original string @returns {string} modified string */ function camelCase (str) { return str.replace(/\-./g, function (s) { return s.substr(1).toUpperCase(); }); } /** test: is item a string? @param {*} item given item to test against @returns {boolean} true, if type of given item is 'string' */ function isStr (item) { return typeof item === 'string'; } /** test: is o an object but not null or Array object? (simple test) @param {*} item given item to test against @returns {boolean} true, if type of given item is 'object' */ function isObj (item) { return item !== null && typeof item === 'object' && !Array.isArray(item); } /** test: is "m" a method of "o"? @param {object} o the given object @param {string} m method name which should be found in object "o" @returns {boolean} true, if object "o" contains a method "m" */ function isMeth (o, m) { var t = typeof o[m]; return ('function|unknown'.indexOf(t) > -1) || (t === 'object' && Boolean(o[m])); } /** create deep copy of an object. IMPORTANT: Simplified, because it will only be used for dElement declaration objects @param {object} o object to be cloned @returns {object} the copy of o */ function oCpy (o) { var no = {}, p, op; for (p in o) { if (hasOwn(o, p)) { op = o[p]; if (Array.isArray(op)) { no[p] = _slice.call(op, 0); } else if (isObj(op)) { no[p] = oCpy(op); } else { no[p] = op; } } } return no; } /* throw error */ dError = (function () { var F; /** throw error @param {string} message error message @class @name dError */ function dErr (message) { var err; if (!this || !(this instanceof Error)) { throw new dError(message); } this.message = 'dElement Error:\n' + message + '\n'; this.name = 'dError'; err = new Error(this.message); err.name = this.name; this.stack = err.stack; console.error(this.message); if (isMeth(console, 'trace')) { console.trace(arguments); } } if (isMeth(Object, 'create')) { dErr.prototype = Object.create(Error.prototype); } else { F = function () {}; F.prototype = Error.prototype; dErr.prototype = new F(); } return dErr; })(); /** map property names of an object. @param {object} o object to be changed @param {object} nmap description object of property names to change in 'o' "oldname": "newname" @returns {object} changed object @example var o = {a: 1, b: 42, c: 'hey'}; // before o = mapNames(o, {a: 'one', c: 'greet'}); // o is now {one: 1, b: 42, greet: 'hey'} */ function mapNames (o, nmap) { var pr; for (pr in nmap) { if (hasOwn(o, pr)) { o[nmap[pr]] = o[pr]; delete o[pr]; } } return o; } /* ================ VARIABLES AND DATA ================= */ /** these properties are processed ahead of any remaining properties to avoid browser bugs (mainly IE of course). Retain order! @type {Array} */ formProps = ['type', 'name', 'value', 'checked', 'selected']; /** skip the following internal properties in createTree() property loop @type {Array} */ skipProps = ['element', 'elrefs', 'clone', 'clonetop']; /** multi-properties. These properties may appear multiple times inside a object declaration, postfixed by an underscore and an unique identifier @type {Array} */ multiProps = ['text', 'event', 'attribute', 'setif', 'html', 'child', 'comment', 'collect']; /** save element reference if one of these props appears @type {Array} */ saveProps = ['id', 'name']; /** recursion counter for variable replacement depth in loop @type {number} */ loopdepth = 0; /** attributes of 'boolean' type. value may be either empty or the attribute name @type {Array} */ boolProps = [ 'checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'selected' ]; /* ============== EVENTS ================= */ /** attach event handler(s) to an element @param {object} evtDcl An object with event data @param {HTMLElement} evtDcl.el target element to attach element to @param {object} evtDcl.val object with function/function name and arguments @param {string|Function} evtDcl.val.func function reference or function name of the event handler @param {Array} evtDcl.val.args arguments to pass to event handler function */ function setEvent (evtDcl) { var ix, fn, o = evtDcl.val, el = evtDcl.el; o = mapNames(o, { 'function': 'func', 'arguments': 'args' }); if (!isObj(o) || !hasOwn(o, 'args')) { throw new dError('Not a valid event declaration', o); } if (!Array.isArray(o.args)) { throw new dError('Expected o.args to be array', o.args); } /* call external, e.g. cross browser event handling o.func is a function reference */ /* o.func is not defined or a string */ if (typeof o.func !== 'function') { /* call method "o.func" of el (defaults to "addEventListener") */ o.func = o.func || 'addEventListener'; fn = el[String(o.func)]; if (typeof fn !== 'function') { throw new dError('eventhandler is not a function/method of element', o); } fn.apply(el, o.args); return; } ix = o.args.indexOf('#el#'); /* placeholder #el# not found, try to find #elp# */ if (ix < 0) { ix = o.args.indexOf('#elp#'); /* placeholder #elp# found */ if (ix >= 0) { if (!el.parentNode) { throw new dError('placeholder #elp# found, but element ' + 'has no parent node', evtDcl); } el = el.parentNode; } else { ix = o.args.indexOf('#elpp#'); /* placeholder #elpp# found */ if (ix >= 0) { if (!el.parentNode || !el.parentNode.parentNode) { throw new dError('placeholder #elpp# found, but element ' + 'has no grandparent node', evtDcl); } el = el.parentNode.parentNode; } } } /* none of the placeholders was found */ if (ix < 0) { /* insert element reference as first argument */ o.args.unshift(el); } /* a placeholder was found */ else { /* insert element reference at defined position */ o.args.splice(ix, 1, el); } /* call event set function */ o.func.apply(el, o.args); } /** add event(s) to event stack @param {HTMLElement} el @param {Array} eo */ function pushEvt (el, eo) { if (!Array.isArray(eo)) { eo = [eo]; } eo.forEach(function (item) { eventStack.push({ el: el, val: item }); }); } /* ============== PROPERTIES ================= */ /** Set a property "prop" of el to "val". Falls back to setAttribute if prop set fails */ function setProp (el, prop, val) { var lcProp = prop.toLowerCase(), rProp; if (lcProp.indexOf('data-') === 0) { /* throw error if data attribute starts with 'data-xml' or contains uppercase letters or semicolon */ if ( lcProp !== prop || /* has uppercase */ lcProp.indexOf('data-xml') > -1 || /* starts with xml */ lcProp.indexOf(';') > -1 ) { throw new dError('data-* property/attribute name may not start with "xml" or' + ' contain any semicolon or uppercase chars'); } /* dataset stores values as string, so it's best to do the same for all data attributes even if dataset API is n/a */ val = String(val); /* set value in dataset API if available. ALWAYS use "in" operator, "hasOwnProperty" returns false on elements */ if ('dataset' in el && isObj(el.dataset)) { el.dataset[camelCase(lcProp.substring(5))] = val; return el; } rProp = lcProp; } else if (boolProps.indexOf(lcProp) > -1) { /* handle attributes which work as a switch (either have no value in HTML or self reference their name as value (e.g. checked="checked")) */ if (val === true || (isStr(val) && val.toLowerCase() === lcProp)) { val = true; } else if (val === false || val === '') { return el; } else { /* not a valid value for boolProps */ throw new dError( 'switch attribute "' + prop + '" has an invalid value of "' + val + '".\nValue may be the attribute\'s name or boolean true only.' ); } rProp = lcProp; } else { /* some attribute names must be replaced, eg. for => htmlFor */ rProp = replaceAttrName(prop); if ( (rProp === 'className' && val === '') || /* prevent empty "className" */ (typeof val === 'boolean' && !val) ) { return el; } } /* set a property, fallback to setAttribute if assignment fails. some old browsers misbehave on "data-" attributes, even in bracket notation */ try { el[rProp] = val; if (el[rProp] !== val && lcProp !== 'href') { /* throw error to apply 'catch' branch */ throw new Error('value type mismatch in property ' + prop); } } catch (ex) { setAttribs(el, {name: rProp, value: val}); } return el; } /** set a property if condition in declaration object is truthy */ function setPropIf (el, pobj) { if (isObj(pobj) && hasOwn(pobj, 'name') && hasOwn(pobj, 'value')) { if (hasOwn(pobj, 'condition') && Boolean(pobj.condition)) { if (pobj.name !== 'child') { setProp(el, pobj.name, pobj.value); } else if (isObj(pobj.value)) { appendTree.call(el, pobj.value); } } } } /** map property names. returns base name of 'multi' properties */ function mapMultiProps (p) { var uscoPos = p.indexOf('_'), base; if (uscoPos > 0) { /* underscore found */ base = p.substring(0, uscoPos); if (multiProps.indexOf(base) > -1) { p = base; } } return p; } /* ============== MISC ================= */ /** set element styles */ function setStyles (el, sty) { if (Array.isArray(sty)) { sty = sty.join(';'); } /* Prefer element.style.cssText if available. */ if (el.style.cssText !== undefined) { el.style.cssText = sty; } else { el.setAttribute('style', sty); } return el; } /** create HTML comment node If document.createComment() is not available, this function adds comment nodes to node elements only */ function addComment (el, comm) { if (Array.isArray(comm)) { comm.forEach(addComment.bind(null, el)); } else { if (isMeth(document, 'createComment')) { /* node element and fragment */ el.appendChild(document.createComment(comm)); } else if (isMeth(el, 'insertAdjacentHTML')) { /* node element only, no fragment */ el.insertAdjacentHTML('beforeEnd', '<!--' + comm + '-->'); } } } /** push function reference from init property and element reference to init stack */ function pushInit (el, val) { if (typeof val === 'function') { initStack.push({ el: el, func: val }); } } /** call functions from init stack 'this' is a reference to the created element tree */ function callInit (fobj) { fobj.func.call(fobj.el, this); } /* ============== LOOPS ================= */ /** loop element creation and replace placeholders */ function loopDecl (s) { var step = 1, start = 0, cnt = 1, parr = ['chk', 'sel'], isdeep = hasOwn(s, 'loopdeep'), frg, i, o, lprop, lobj, lcnt; if (hasOwn(s, 'loop') && isdeep) { throw new dError('You may use only one of "loop" OR "loopdeep", not both.'); } lprop = (isdeep) ? 'loopdeep' : 'loop'; lobj = s[lprop]; delete s[lprop]; /* check if lobj is either a valid object or a numeric value */ if (isObj(lobj) && hasOwn(lobj, 'count') && !isNaN(lobj.count)) { /* validate loop values count, step and start */ cnt = Number(lobj.count); if (hasOwn(lobj, 'step') && !isNaN(lobj.step)) { step = Number(lobj.step); if (step === 0) { step = 1; } } if (hasOwn(lobj, 'start') && !isNaN(lobj.start)) { start = Number(lobj.start); } /* validate 'values' array (for "v" placeholder) */ if (hasOwn(lobj, 'values')) { if (!Array.isArray(lobj.values)) { throw new dError('loop property "values" has to be an array'); } if (!hasOwn(lobj, 'valuesrepeat') && lobj.values.length < lobj.count) { throw new dError( '"values" array has less elements (' + lobj.values.length + ') than loop count (' + lobj.count + ').\nAdd more items to' + ' the array or set "valuesrepeat" mode.' ); } } /* validate chk/sel properties (checked or selected elements) */ parr.forEach(function (item) { if (hasOwn(lobj, item)) { if (!Array.isArray(lobj[item]) && isNaN(lobj[item])) { throw new dError( 'type of loop property "' + item + '" must be array or number' ); } } }); } else if (!isNaN(lobj)) { cnt = Number(lobj); } cnt = Math.abs(Math.round(cnt)); /* make count a positive integer */ frg = document.createDocumentFragment(); /* element loop */ lcnt = 0; for (i = start; i < (start + (step * cnt)); i += step) { if (Math.floor(i) !== i) { /* float check */ i = parseFloat(i.toFixed(8), 10); /* avoid rounding errors */ } /* replace placeholders with current values */ o = replaceCounter({ declaration: s, value: i, counter: lcnt, recursive: isdeep, config: lobj }); /* set checked/selected if one of the properties from "parr" exists */ if (parr.some(hasOwn.bind(null, lobj))) { o = setCSFlags({ declaration: o, config: lobj, loopcount: lcnt, properties: parr }); } /* create element tree and append to fragment */ appendTree.call(frg, o); lcnt++; } // TEMP: wait ... why did I deactivate this line? // s[lprop] = lobj; return frg; } /** find placeholders and replace them with committed values @param {object} argObj object with required values @param {object} argObj.declaration dElement declaration object @param {number} argObj.value calculated value @param {number} argObj.counter loop counter @param {boolean} argObj.recursive recursive replace in subdeclarations @param {object} argObj.config loop configuration object @returns {object} declaration with replaced values */ function replaceCounter (argObj) { var i = argObj.value, c = argObj.counter, isdeep = argObj.recursive, lobj = argObj.config, o = oCpy(argObj.declaration), /* create copy of declaration */ phreg, p, cc, v; /* RegExp to match all parts of "n" and "c" placeholders */ phreg = /\!\!(?:([+-]?\d+(?:\.\d+)?)[•\*]?)?(n|c)([+-]\d+(?:\.\d+)?)?\!\!/gi; /* !! | mul number |mul sign| nc | add/sub number | !! */ /* | [1] | | [2]| [3] | */ /* handle array index if "values" propery is an array */ if (hasOwn(lobj, 'values')) { v = lobj.values; cc = (hasOwn(lobj, 'valuesrepeat')) ? c % v.length : c; } for (p in o) { /* replace all placeholders in string */ if (isStr(o[p])) { /* replace each "v" placeholder with array value */ if (Array.isArray(v) && o[p].indexOf('!!v!!') > -1) { o[p] = o[p].replace( /\!\!v\!\!/gi, v[cc] ); } /* replace each "n" or "c" placeholder with its calculated value */ o[p] = o[p].replace( phreg, loopReplace.bind(null, c, i) ); } else if ( /* scan for placeholders in subdeclarations until a loop, loopstop or loopdeep property is found or loop depth exceeds 1 on loop property (but always replace placeholders in first child declaration [loopdepth = 0]) */ p === 'child' && isObj(o.child) && !hasOwn(o.child, 'loop') && !hasOwn(o.child, 'loopdeep') && !hasOwn(o.child, 'loopstop') && (isdeep || loopdepth < 1) ) { loopdepth++; /* increase depth counter */ o.child = replaceCounter({ declaration: o.child, value: i, counter: c, recursive: isdeep, config: lobj }); loopdepth--; } } return o; } /** callback for op.replace in function replaceCounter: calculate value of placeholder and replace it. @param {number} cnt loop counter @param {number} val loop value @param {string} matched matched string (not used) @param {string} mul multiplier (can include leading "+" or "-") @param {string} ptype placeholder type (n or c for loop value or counter) @param {string} add sum to add (can include leading "+" or "-") @return {number} final calculated value for placeholder replacement */ function loopReplace (cnt, val, matched, mul, ptype, add) { /* determine type of value */ var cv = (ptype.toLowerCase() === 'c') ? cnt /* loop counter */ : val; /* calculated value */ mul = Number(mul); if (!isNaN(mul)) { cv *= mul; } add = Number(add); if (!isNaN(add)) { cv += add; } return cv; } /** set checked or selected property in declaration @param {object} argObj.declaration original dElement declaration object @param {object} argObj.config loop configuration object @param {number} argObj.loopcount loop counter @param {array} argObj.properties array of property names to process @returns {object} declaration object with replaced values */ function setCSFlags (argObj) { var o = argObj.declaration, lobj = argObj.config, lc = argObj.loopcount, arr = argObj.properties, i = arr.length, c = lc + 1, item, prp; while (i--) { item = arr[i]; if (hasOwn(lobj, item) && ( c === lobj[item] || (Array.isArray(lobj[item]) && lobj[item].indexOf(c) > -1) )) { prp = (item === 'sel') ? 'selected' : 'checked'; o[prp] = true; } } return o; } /* ============== ELEMENT REFERENCES ================= */ /** add current element reference to collection */ function collectElRef (sc, el) { if (Array.isArray(sc)) { sc.push(el); } else if (isObj(sc) && hasOwn(sc, 'obj') && hasOwn(sc, 'name') && isObj(sc.obj)) { if (sc.obj[sc.name] === undefined) { sc.obj[sc.name] = el; } else { throw new dError( 'duplicate declaration of ' + sc.name + ' in property "collect"' ); } } else { throw new dError('Value of property "collect" must be an array or an object' + ' containing the properties "obj" and "name".'); } } /** save references of elements with name or id property */ function saveRefs (rObj, sdata) { var pr = sdata.s[sdata.p]; if (sdata.lp === 'id') { rObj.i[pr] = sdata.el; } else if (sdata.lp === 'name') { if (Array.isArray(rObj.n[pr])) { rObj.n[pr].push(sdata.el); } else { rObj.n[pr] = [sdata.el]; } } return rObj; } /* ============== ATTRIBUTES ================= */ /** replace special property names */ function replaceAttrName (atn) { var lcAtt = atn.toLowerCase(); return (lcAtt in attrNames) ? attrNames[lcAtt] : camelCase(atn); } /** set attributes with setAttribute(). will be used only when enforced by property attr|attrib|attribute or with certain problematic properties value has to be either - an object containing properties value and name e.g. attribute: {name: 'foo', value: 'bar'} - an array with objects as described above */ function setAttribs (el, att) { if (Array.isArray(att)) { att.forEach(setAttribs.bind(null, el)); } else if ( isNode(el) && isObj(att) && hasOwn(att, 'name') && hasOwn(att, 'value') && isStr(att.name) ) { if (att.name.toLowerCase() === 'style') { /* setAttribute with "style" fails in some browsers, handle it */ setStyles(el, att.value); } else { el.setAttribute(att.name, att.value); } } return el; } /* ============== PARSE EXTENDED SYNTAX ================= */ /* parseElemStr() */ parseElemStr = (function () { var ETX = '\x03', /* 0x03 (ETX, end of text) */ US = '\x1F', /* 0x1F (US, unit separator) */ WSP = ' \t\r\n\f\x0B\xA0', /* some whitespace */ MODE_END = ETX, MODE_TAGNAME = '$', MODE_ID = '#', MODE_CLASS = '.', MODE_NAME = '~', MODE_TYPE = '@', MODE_VALUE = '=', modeChars = {}, moc = '', stopChars, pr; /* data for parse modes defaults are unique:true and stop:true */ modeChars[MODE_TAGNAME] = {}; modeChars[MODE_END] = {}; modeChars[MODE_CLASS] = {attrName: 'class', unique: false}; modeChars[MODE_ID] = {attrName: 'id'}; modeChars[MODE_NAME] = {attrName: 'name'}; modeChars[MODE_TYPE] = {attrName: 'type'}; modeChars[MODE_VALUE] = {attrName: 'value', stop: false}; for (pr in modeChars) { if (hasOwn(modeChars, pr)) { moc += pr; } } /** mode and illegal chars. This is not a RegExp; multichar sequences like \s \b \w will not work */ stopChars = moc + US + WSP + '%<>*\'"/|\\?^!§&()[]{}+:,;'; /** wrapper for parser errors */ function pError (str) { throw new dError('Parser error: ' + str); } /** * callback for [].filter in joinClassNames: detect duplicate elements in array * * @param {$_TYPE_$} cn * * @param {$_TYPE_$} ix * * @param {$_TYPE_$} arr * * @returns {$_TYPE_$} * */ function removeDupes (cn, ix, arr) { return ix === arr.indexOf(cn); } /** */ function isStopMode (mo) { return !mo || !hasOwn(modeChars, mo) || !hasOwn(modeChars[mo], 'stop') || modeChars[mo].stop !== false; } /** join class names from extended syntax and className property, remove duplicates @param {object} dcl element declaration @param {Array} cArr array with class names from parse @returns {string} space separated class names as string */ function joinClassNames (dcl, cArr) { if (hasOwn(dcl, 'className')) { cArr = cArr.concat(dcl.className.split(/\s+/)); } return (cArr.length > 1) ? cArr.filter(removeDupes).join(' ') : cArr[0]; } /** parseElemStr() parse element string for id, name, class names, value and type and create correlating properties @param dcl {object} current element tree declaration object @returns {object} altered element tree declaration object @function @name parseElemStr */ return function (dcl) { var mode = MODE_TAGNAME, /* default mode: string starts with tagname */ str = dcl.element, /* string to parse */ part = '', /* parsed word */ clNames = [], /* collects class names */ i = 0, cnt = {}, stop = true, /* wether to stop on one of stopChars */ ch, /* current char */ mcc, len; /* init counters */ for (mcc in modeChars) { if (hasOwn(modeChars, mcc)) { cnt[mcc] = 0; } } /* remove possible ETX, leading and trailing whitespace in string and append ETX end marker */ str = str.replace(/^\s*(.*)\s*$/g, '$1').replace(ETX, '') + ETX; len = str.length; /* if first char is in modeChars, "str" doesn't start with element name */ ch = str.charAt(0); if (hasOwn(modeChars, ch)) { mode = ch; i++; if (!((/\$[a-z][a-z1-6]?/i).test(str))) { /* tag name not defined */ pError('extended syntax without element node name definition\n"' + str + '"'); } } while (mode !== MODE_END) { ch = str.charAt(i); i++; /* ignore whitespace unless it's within a no stop declaration */ if (WSP.indexOf(ch) >= 0 && isStopMode(mode)) { continue; } /* when in a no stop mode: restore stop mode if ETX or US char is found or if end of string and ignore everything until mode change char is detected */ if (stop === false && (ch === US || ch === ETX || i >= len)) { stop = true; /* ignore all chars until the next mode changing char appears or the string ends */ while (i < len && !(ch in modeChars)) { ch = str.charAt(i); i++; } } /* if "ch" is not a stopchar or a no stop mode is activated, append "ch" to "part" and continue with next char */ if (stopChars.indexOf(ch) < 0 || stop === false) { part += ch; continue; } /* if length of "part" is 0, "str" is not valid */ if (part.length === 0) { pError('empty value in mode "' + mode + '" "' + str.slice(0, -1) + '"'); } /* set property based on current parse mode and reset "part" to empty for for next string */ switch (mode) { case MODE_TAGNAME: dcl.element = part; cnt[MODE_TAGNAME]++; break; case MODE_CLASS: clNames.push(part); break; default: /* find matching attribute name, create property and set its value */ if (hasOwn(modeChars, mode) && hasOwn(modeChars[mode], 'attrName')) { dcl[modeChars[mode].attrName] = part; } else { pError('mode not supported: "' + mode + '"'); } break; } part = ''; /* set new mode based on stop char or throw Error on illegal char */ if (hasOwn(modeChars, ch)) { mcc = modeChars[ch]; /* count the usage of each attribute declaration. if 'unique' is set, the count may not exceed 1 */ cnt[ch]++; if (cnt[ch] > 1 && (!hasOwn(mcc, 'unique') || mcc.unique !== false)) { if (ch === MODE_TAGNAME) { pr = ' tag name'; } else if (hasOwn(mcc, 'attrName')) { pr = mcc.attrName; } else { pr = ' (unknown property)'; } pError('element may not have more than one ' + ch + pr + '.\n\t"' + str + '"'); } mode = ch; stop = !hasOwn(mcc, 'stop') || Boolean(mcc.stop); } else { pError('Illegal char: "' + ch + '" (' + ch.charCodeAt(0) + ') in "' + str.slice(0, -1) + '" at position ' + i); } } /* while (mode...) */ /* combine class names and remove dupes */ if (clNames.length > 0) { dcl.className = joinClassNames(dcl, clNames); } /* altered declaration */ return dcl; }; })(); /* =================== NODE TREE FUNCTIONS ====================== */ /** convert "txt" to DOM text node */ function textNode (txt) { if (Array.isArray(txt)) { return document.createTextNode(txt.join('')); } return isTextNode(txt) ? txt : document.createTextNode(txt); } /** convert HTML string (as used with innerHTML) to DOM node tree. @function @param {string} hstr (hopefully valid) HTML string @returns {HTMLElement|DocumentFragment} created DOM Element/Fragment with child nodes */ strToNodes = (function () { var el = document.createElement('template'), isTmpl = 'content' in el, createParent; if (isTmpl) { /* use HTML <template> element if available */ createParent = function () { return document.createElement('template'); }; } else { /* if HTML element 'template' is not available, determine best matching parent element or use 'div' otherwise */ createParent = (function (parlist, str) { var elp = 'div', m = str.match(/\<\s*([a-z][a-z1-6]*)/i); if (m && m.length > 1 && hasOwn(parlist, m[1])) { elp = parlist[m[1]]; } return document.createElement(elp); }).bind(null, { /* define parent elements for some element types. */ tr: 'tbody', tbody: 'table', thead: 'table', th: 'tr', td: 'tr', tfoot: 'table', caption: 'table', 'option': 'select', li: 'ul', dd: 'dl', dt: 'dl', optgroup: 'select', figcaption: 'figure', menuitem: 'menu', legend: 'fieldset', summary: 'details' }); } return function (hstr) { var d = createParent(hstr), txt = '', frg, fc; /* Applying innerHTML directly to an fragment doesn't work. Using a dummy element and then moving it's child nodes to the fragment does the trick. */ try { d.innerHTML = hstr; /* if 'el' is a <template> element, it's 'content' property already references a documentFragment and we're done */ if (isTmpl) { frg = d.content; } else { /* move all elements to fragment */ frg = document.createDocumentFragment(); while ((fc = d.firstChild)) { frg.appendChild(fc); } } } catch (ex) { /* assigning "str" with innerHTML will fail if content-type of document is application/xhtml+xml AND either named entities other than >, <, &, ", &apos are used OR if "str" contains certain illegal HTML */ if (ex.code === 12) { txt = 'ERROR.\nHTML string most likely contains illegal HTML or ' + 'uses named entities (restricted when using content-type ' + 'application/xhtml+xml.\nuse numeric entities instead)\n\n'; } throw new dError( txt + 'Error code: ' + ex.code + '\nError message: ' + ex.message + '\nContent (leading 200 chars):\n"' + hstr.substring(0, 200) + '…"' ); } /* return fragment with children */ return frg; }; })(); /** appends elements, DOM tree or HTML string to a given parent node element. @param {HTMLElement} el @param {*} item @returns {HTMLElement} @throws {dError} if type of item to be added is not allowed */ function addNodes (el, item) { if (isAppendable(item)) { el.appendChild(item); } /* handle HTML string */ else if (isStr(item)) { if (item === '') { return el; } if (isEl(el)) { /* a HTML element */ el.insertAdjacentHTML('beforeend', item); } else if (isMeth(el, 'appendChild')) { /* a fragment, ... */ el.appendChild(strToNodes(item)); } } else if (Array.isArray(item)) { item.forEach(addNodes.bind(null, el)); } else { throw new dError('illegal type of property (' + item + ')'); } return el; } /** create DOM tree and append to object expects parent element as 'this', e.g. by calling it: appendTree.call(el, dcl) @param {object} dcl a dElement declaration object @returns {HTMLElement} created element or element array @this {HTMLElement} Element to append created element(s) to. */ function appendTree (dcl) { var s = createTree(dcl); if (s) { this.appendChild(s); } return s; } /** append child nodes to an element. @param {HTMLElement} el root element to append created content to @param {*} sp content to append. Can be an element(tree), a dElement declaration object, a text or an array of all of the former. */ function appendChildNodes (el, sp) { if (Array.isArray(sp)) { sp.forEach(appendChildNodes.bind(null, el)); } else if (isAppendable(sp)) { el.appendChild(sp); } else if (isObj(sp)) { appendTree.call(el, sp); } else { el.appendChild(textNode(sp)); } } /** clone a node(tree) or a declaration^ @param {object} s dElement declaration object or node tree @throws dError if cloning failed */ function cloneObject (s) { var scl, el, clo = hasOwn(s, 'clone'); if (clo && hasOwn(s, 'clonetop')) { throw new dError('only one of "clone" or "clonetop" may be used.'); } scl = s.clone || s.clonetop; if (isAppendable(scl) && isMeth(scl, 'cloneNode')) { el = scl.cloneNode(clo); /* clone: true, clonetop: false */ } else if (isObj(scl)) { el = createTree(scl); } if (!isAppendable(el)) { throw new dError('this object can\'t be cloned: ' + scl); } return el; } /** test for declaration of children for an empty content type element @param {object} s dElement declaration object @throws dError if a empty element has children */ function testVoidAppend (s) { var pp = ['text', 'html', 'child'], prop, lcProp; for (prop in s) { lcProp = mapMultiProps(prop.toLowerCase()); if (pp.indexOf(lcProp) > -1) { throw new dError('content model of element "' + s.element.toUpperCase() + '" is "empty". This element may not contain any child nodes'); } } } /** if a declaration "s" does not contain a property "element", then there MUST be a property "html", "text" or "comment". @param {object} s declaration object @returns {DocumentFragment} document fragment containing nodes/node tree @throws dError if neither of the required properties are declared */ function noElementDeclaration (s) { var frg = document.createDocumentFragment(), c = 0, prop, lcProp; /* handle multi properties 'text', 'comment' and 'html' */ for (prop in s) { lcProp = mapMultiProps(prop.toLowerCase()); switch (lcProp) { case 'text': frg.appendChild(textNode(s[prop])); break; case 'html': addNodes(frg, s[prop]); c++; break; case 'comment': addComment(frg, s[prop]); c++; break; } } /* throw error, if no required property has been defined in s, but skip, if either "comm(ent)" or "html" is the only property of "s" and html is empty or comment couldn't be added (fragment still empty) */ if (!frg.hasChildNodes() && c !== 1) { throw new dError( 'Every (sub)declaration object requires at least one of the following ' + 'properties:\n"element", "text", "clone", "clonetop", "comment" or "html".' ); } /* return, if documentFragment has childNodes */ return frg; } /** process declaration object (recursive) and create element tree @param {object} s declaration object @returns {HTMLElement|DocumentFragment|Text|Comment} created node(s)/node tree @throws dError in case of an unrecoverable error. */ function createTree (s) { var frg, lcProp, newEl, prop, sp; /* array of element declarations without common parent element */ if (Array.isArray(s)) { frg = document.createDocumentFragment(); s.forEach(appendTree, frg); return frg; } /* s is a node element, fragment or text node */ if (isAppendable(s)) { return s; } /* s is text string or numeric */ if (isStr(s) || !isNaN(s)) { return textNode(s); } /* return cloned node */ if (hasOwn(s, 'clone') || hasOwn(s, 'clonetop')) { return cloneObject(s); } /* loop: duplicate elements n times and replace placeholder */ if (hasOwn(s, 'loop') || hasOwn(s, 'loopdeep')) { return loopDecl(s); } /* normalize shortcut keywords */ s = mapNames(s, { cond: 'condition', comm: 'comment', attrib: 'attribute', attr: 'attribute' }); /* stop processing if property 'condition' exist and it's value is falsy */ if (hasOwn(s, 'condition') && !s.condition) { return null; } /* init or unset reference declaration data */ if (s.elrefs === null) { refs = null; } else if (isObj(s.elrefs)) { refs = s.elrefs; refs.i = refs.i || {}; /* collect elements with id */ refs.n = refs.n || {}; /* collect elements with name */ } delete s.elrefs; /* if a declaration object doesn't include a property named 'element' then one of either 'text', 'html', 'comment', 'clone', 'clonetop' MUST be defined. 'clone' and 'clonetop' are processed above; this part takes care of 'html', 'text', 'comment'. */ if (!hasOwn(s, 'element')) { return noElementDeclaration(s); } /* s.element holds the name of the element to be created */ if (!isStr(s.element)) { throw new dError('type of property "element" must be string'); } /* uses string parser if value of "s.element" contains chars other than a-z1-6 */ if ((/[^a-z1-6]/i).test(s.element)) { s = parseElemStr(s); } /* test for declaration of child nodes for empty content type elements */ if (voidElems.indexOf(s.element) > -1) { testVoidAppend(s); } /** create Element and set certain properties before main loop to avoid some browser bugs. Bug in IE8 and Opera <= 12: If "type" is not assigned as first property on "input" (and maybe other) elements, the "value" property might be lost. */ newEl = document.createElement(s.element); formProps.forEach(function (pr) { if (hasOwn(s, pr)) { setProp(newEl, pr, s[pr]); delete s[pr]; } }); /* loop all properties */ for (prop in s) { lcProp = prop.toLowerCase(); /* save element reference when prop is in saveProps */ if (isObj(refs) && saveProps.indexOf(lcProp) > -1) { refs = saveRefs(refs, { s: s, el: newEl, p: prop, lp: lcProp }); } /* skip props 'element', 'elrefs', 'clone', 'clonetop' */ if (skipProps.indexOf(lcProp) > -1) { continue; } /* handle name of a multi-property */ lcProp = mapMultiProps(lcProp); sp = s[prop]; /* handle property value according to property name */ switch (lcProp) { /* collect element references */ case 'collect': collectElRef(sp, newEl); break; /* create a text node */ case 'text': newEl.appendChild(textNode(sp)); break; /* create a comment node */ case 'comment': addComment(newEl, sp); break; /* process sub declaration object or declaration array */ case 'child': appendChildNodes(newEl, sp); break; /* add eventhandler to current element */ case 'event': pushEvt(newEl, sp); break; /* process html string */ case 'html': addNodes(newEl, sp); break; /* enforced usage of setAttribute() */ case 'attribute': setAttribs(newEl, sp); break; /* css styles */ case 'style': setStyles(newEl, sp); break; /* set property on condition */ case 'setif': setPropIf(newEl, sp); break; /* init functions */ case 'init': case 'initnorun': pushInit(newEl, sp); break; /* anything else will be treated as property/attribute of newEl */ default: setProp(newEl, prop, sp); break; } } return newEl; } /* ============== dElement() & dAppend() ================= */ /** dElement: create node tree from declaration object @param {object} decl element declaration object @returns {HTMLElement|DocumentFragment|null} node tree or fragment or null */ K345.dElement = function (decl) { var dtree; if (!Array.isArray(decl) && !isObj(decl)) { throw new dError('Parameter has been omitted or value is not an object/array'); } /** collect events @type {Array} */ eventStack = []; /** collect init functions @type {Array} */ initStack = []; /** collect element references @type {object} */ refs = null; dtree = createTree(decl); if (dtree) { eventStack.forEach(setEvent); initStack.forEach(callInit, dtree); } return dtree || null; }; /** dAppend: create node tree and append it to an existing element @param {HTMLElement|string} elem element reference or id as string @param {object} decl element declaration object (see dElement) @param {string|number} [mode=K345.DAPPEND_APPEND] set append mode <pre> possible values for mode: 'beforeEnd' or K345.DAPPEND_LAST or K345.DAPPEND_APPEND => append to 'elem' (default) 'beforeBegin' or K345.DAPPEND_BEFORE => insert before 'elem' 'afterEnd' or K345.DAPPEND_AFTER => insert after 'elem' 'replaceElement' or K345.DAPPEND_REPLACE => replace existing element 'afterBegin' or K345.DAPPEND_FIRST => append as first child of 'elem' 'wipeContent' or K345.DAPPEND_WIPE => wipe existing child elements and append as child of 'elem' mode values MAY NOT be combined!</pre> @returns {HTMLElement|DocumentFragment|null} node tree or null */ K345.dAppend = function (elem, decl, mode) { var nodes = null, elc; if (isStr(elem)) { /* if one of the regex test chars is found in "elem" string, it's not a id string. might be a css like selector */ elem = (dAppend_regex.test(elem)) ? document.querySelector(elem) : document.getElementById(elem); } if (isNode(elem)) { nodes = K345.dElement(decl); if (!nodes) { return null; } switch (mode) { case 'beforeBegin': case K345.DAPPEND_BEFORE: elem.parentNode.insertBefore(nodes, elem); break; case 'afterEnd': case K345.DAPPEND_AFTER: elem.parentNode.insertBefore(nodes, elem.nextSibling); break; case 'replaceElement': case K345.DAPPEND_REPLACE: elem.parentNode.replaceChild(nodes, elem); break; case 'afterBegin': case K345.DAPPEND_FIRST: elem.insertBefore(nodes, elem.firstChild); break; case 'wipeContent': case K345.DAPPEND_WIPE: while ((elc = elem.lastChild)) { elem.removeChild(elc); } case 'beforeEnd': case K345.DAPPEND_APPEND: case K345.DAPPEND_LAST: default: elem.appendChild(nodes); break; } } return nodes; }; /** append as last child of element (default). mode flag for {@link K345.dAppend()} @constant */ K345.DAPPEND_APPEND = 0; /** append as last child of element (default). mode flag for {@link K345.dAppend()} @constant */ K345.DAPPEND_LAST = 0; /** insert before element. mode flag for {@link K345.dAppend()} @constant */ K345.DAPPEND_BEFORE = 1; /** insert after element. mode flag for {@link K345.dAppend()} @constant */ K345.DAPPEND_AFTER = 2; /** replace element. mode flag for {@link K345.dAppend()} @constant */ K345.DAPPEND_REPLACE = 3; /** append as first child. mode flag for {@link K345.dAppend()} @constant */ K345.DAPPEND_FIRST = 4; /** wipe all existing child nodes and append. mode flag for {@link K345.dAppend()} @constant */ K345.DAPPEND_WIPE = 5; })(K345.attrNames, K345.voidElements);