Coolworx67
3/25/2017 - 2:34 PM

createDOM.js

function createDOM(domObj) {
  'use strict';
  // 2017.03.12
  var
    dom = {},
    eventTypes = ['click', 'focus', 'blur',
      'mouseover', 'mouseout', 'submit', 'reset',
      'keydown', 'keypress', 'keyup', 'change',
      'mousedown', 'mouseup', 'dblclick'
    ],
    cssPseudoClasses = ['hover', 'focus', 'active', 'checked'],
    cssPseudoElements = ['after', 'before', 'firstLetter',
      'firstLine', 'selection'
    ];

  dom.delimiter = domObj.delimiter || '|';

  if (Array.isArray(domObj)) {
    dom = {
      tag: 'FRAGMENT',
      desc: domObj
    };
  } else if (typeof domObj === 'string') {
    dom = Object.assign(dom, parseTag(domObj.replace(/\s\s+/g, ' ')));
  } else if (typeof domObj === 'object') {
    dom = Object.assign(dom, domObj);
  }
  
  if (dom.className) {
    if (Array.isArray(dom.className)) {
  	  dom.className = dom.className.join(' ');    
    }
    dom.className = dom.className.replace(/,/g, ' ').replace(/\s\s+/g, ' ');
  }

  if (dom.tag) {
    var parsedTag = parseTag(dom.tag);
    if (parsedTag.className && dom.className) {
      parsedTag.className += ' ' + dom.className;      
    }
    dom = Object.assign(dom, parsedTag);
  } else dom.tag = 'DIV';


  if (dom.scopedCSS) {
    var tmpScopedCSS = Object.assign({}, dom.scopedCSS);
    delete dom.scopedCSS;
    dom = {
      tag: 'SPAN',
      style: {
      	display: 'inline',
        margin: 0,
        padding: 0
      },
      className: scopeCSS(tmpScopedCSS),
      desc: dom
    };
  }

  var
    cleanTag = dom.tag.trim().toUpperCase(),
    elm = (cleanTag === 'FRAGMENT') ?
      document.createDocumentFragment() : document.createElement(cleanTag);

  eventTypes.forEach(function(etype) {
    if (dom[etype]) {
      if (!dom.events) dom.events = {};
      dom.events[etype] = dom[etype];
      delete dom[etype];
    }
  });

  if (dom.html || dom.desc || dom.text) {
    var appendCalls = {
      html: function() {
        elm.innerHTML = elm.innerHTML + dom.html;
      },
      text: function() {
        elm.appendChild(document.createTextNode(dom.text));
      },
      desc: function() {
        if (Array.isArray(dom.desc)) dom.desc.forEach(function(i) {
          appendElm(i);
        });
        else appendElm(dom.desc);
      }
    };

    Object.keys(dom).forEach(function(k) {
      if (appendCalls.hasOwnProperty(k)) {
        appendCalls[k]();
        delete dom[k];
      }
    });
  }

  if (dom.style) {
    if (typeof dom.style === 'string') {
      setAttr('style', dom.style);
    } else if (typeof dom.style === 'object') {
      Object.keys(dom.style).forEach(function(s) {
        elm.style[s] = isNumber(dom.style[s]) ? dom.style[s] += 'px' : dom.style[s];
      });
    }
    delete dom.style;
  }

  if (dom.events) {
    Object.keys(dom.events).forEach(function(evt) {
      if (typeof window.addEventListener === 'function')
        elm.addEventListener(evt, dom.events[evt], false);
      else if (typeof window.attachEvent === 'function')
        elm.attachEvent('on' + evt, dom.events[evt]);
      else elm['on' + evt] = dom.events[evt];
      if (dom.registerEvents) {
        dom.registerEvents(elm, evt, dom.events[evt]);
      }
    });
    delete dom.events;
    delete dom.registerEvents;
  }

  delete dom.delimiter;
  delete dom.tag;

  Object.keys(dom).forEach(function(k) {
    var prop = checkReplaceHash(k);
    setAttr(prop, dom[k]);
  });

  return elm;

  // Sub Functions

  function isNumber(val) {
    return Number(parseFloat(val)) === val;
  }

  function isElm(el) {
    return (el.nodeType && el.nodeType === 1);
  }

  function appendElm(el) {
    if (isElm(el)) elm.appendChild(el);
    else {
      var childElm = (typeof el === 'object') ?
        Object.assign({}, el) : (typeof el === 'string') ?
        Object.assign({}, parseTag(el.replace(/\s\s+/g, ' '))) : {};

      childElm.delimiter = childElm.delimiter || dom.delimiter;
      elm.appendChild(createDOM(childElm));
    }
  }

  function setAttr(key, val) {
    try {
      if (key === 'class') {
        val.split(' ').forEach(function(c) {
          elm.classList.add(c);
        });
      } else elm.setAttribute(key, val);
    } catch (err) {
      console.log(err);
    }
  }

  function makeUniqueKey() {
    var num = 2;
    var res = '';
    while (!!num) {
      var array = new Uint32Array(num);
      window.crypto.getRandomValues(array);
      for (var i = 0; i < array.length; i++) {
        res += array[i].toString(36);
      }
      num--;
    }
    return '_' + res;
  }

  function camelToHyphen(s) {
    var res = s.replace(/[a-z][A-Z]/g, function(str, offset) {
      return str[0] + '-' + str[1].toLowerCase();
    });
    return res;
  }

  function jsToCSS(o) {
    var res = '';
    Object.keys(o).forEach(function(k) {
      if (isNumber(o[k])) o[k] += 'px';
      res = res.concat(camelToHyphen(k) + ':' + o[k] + ';');
    });
    return res;
  }

  function parseTag(str) {
    var
      attrs = str.split(dom.delimiter),
      tagString = attrs.shift(),
      res = {},
      symHash = {
        '.': 'className',
        '#': 'id',
        '~': 'name',
        '^': 'type'
      },
      flags = Object.keys(symHash),
      prop = 'tag',
      classStart = false;

    for (var ii = 0; ii < tagString.length; ii++) {
      if (!flags.includes(tagString[ii])) {
        if (prop === 'className' && classStart) {
          res.className = res.className ? res.className + ' ' : '';
          classStart = false;
        }
        res[prop] = res[prop] ? res[prop] + tagString[ii] : tagString[ii];
        continue;
      } else {
        prop = symHash[tagString[ii]];
        if (prop === 'className') classStart = true;
      }
    }

    if (attrs.length > 0) {
      attrs.forEach(function(kp) {
        var p = kp.split('=');
        if (p[0].trim() !== '') {
          res[p[0].trim()] = (p[1] && p[1].trim) ? p[1].trim() : '';
        }
      });
    }
    res.tag = res.tag || 'DIV';
    return res;
  }

  function createStyleSheet() {
    if (document.getElementById('createDOM_Generated_Style_Sheet')) {
      return document.getElementById('createDOM_Generated_Style_Sheet');
    } else {
      var style = createDOM({
        tag: 'style',
        type: 'text/css',
        media: 'screen',
        id: 'createDOM_Generated_Style_Sheet'
      });

      document.getElementsByTagName('head')[0].appendChild(style);
      return style;
    }
  }

  function writeStyleSheet(o) {
    var style = createStyleSheet();

    if (!(style.sheet || {}).insertRule) {
      (style.styleSheet || style.sheet).addRule(o.name, o.rules);
    } else {
      style.appendChild(document.createTextNode(o.name + " {" + o.rules + "}"));
    }
  }

  function writeKeyframes(keyframeObj, name) {
    var style = createStyleSheet();
    var str = name + '{';

    Object.keys(keyframeObj).forEach(function(frame) {
      str += frame + '{';
      str += jsToCSS(keyframeObj[frame]) + '}';
    });

    str += '}';
    style.appendChild(document.createTextNode(str));
  }

  function writeMediaQuery(cls, css, mediaObj) {
    var style = createStyleSheet();
    var str = mediaObj + '{';

    Object.keys(css[mediaObj]).forEach(function(mediaRule) {
      var mRule = css[mediaObj][mediaRule];

      Object.keys(mRule).forEach(function(mr) {

        if (cssPseudoClasses.includes(mr) || cssPseudoElements.includes(mr)) {
          var colons = cssPseudoElements.includes(mr) ? '::' : ':';
          str += '.' + cls + ' ' + mediaRule + colons + mr + ' ' + '{';
          str += jsToCSS(css[mediaObj][mediaRule][mr]) + '}';
          delete css[mediaObj][mediaRule][mr];
        }

      });

      str += '.' + cls + ' ' + mediaRule + ' ' + '{';
      str += jsToCSS(css[mediaObj][mediaRule]) + '}';
    });

    str += '}';
    style.appendChild(document.createTextNode(str));

  }


  function scopeCSS(css) {
    var rootClass = makeUniqueKey();

    Object.keys(css).forEach(function(item) {

      if (item.substr(0, '@keyframes'.length) === '@keyframes') {
        writeKeyframes(css[item], item);
        return;
      }

      if (item.substr(0, '@media'.length) === '@media') {
        writeMediaQuery(rootClass, css, item);
        return;
      }

      Object.keys(css[item]).forEach(function(prop) {
        if (cssPseudoClasses.includes(prop) || cssPseudoElements.includes(prop)) {
          var colons = cssPseudoElements.includes(prop) ? '::' : ':';

          if (prop === 'selection') {
            writeStyleSheet({
              name: '.' + rootClass + ' ' + camelToHyphen(item) + colons + '-moz-' + camelToHyphen(prop),
              rules: jsToCSS(css[item][prop])
            });
          }

          writeStyleSheet({
            name: '.' + rootClass + ' ' + camelToHyphen(item) + colons + camelToHyphen(prop),
            rules: jsToCSS(css[item][prop])
          });
          delete css[item][prop];
          return;
        }
      });

      writeStyleSheet({
        name: '.' + rootClass + ' ' + camelToHyphen(item.replace(/\$/g, ':')),
        rules: jsToCSS(css[item])
      });
      return;
    });

    return rootClass;
  }

  function checkReplaceHash(prop) {

    var replaceHash = {
      'className': 'class',
      'data_': 'data-',
      '@': 'v-on:',
      'v_on_': 'v-on:',
      'v_bind_': 'v-bind:',
      'v_if': 'v-if',
      'v_for': 'v-for',
      'v_text': 'v-text',
      'v_html': 'v-html',
      'v_else_if': 'v-else-if',
      'v_else': 'v-else',
      'v_show': 'v-show',
      'v_pre': 'v-pre',
      'v_model': 'v-model',
      'v_once': 'v-once',
      'v_cloak': 'v-cloak'
    };

    var res = prop;

    Object.keys(replaceHash).forEach(function(k) {
      if (prop.startsWith(k)) {
        res = prop.replace(k, replaceHash[k]);
        if (res.startsWith('v-on:') && res.includes('$')) {
          res = res.replace(/\$/g, '.');
        }
        return;
      }
    });

    return res;
  }
}