mini-js
Version:
Mini Js is a Javascript library/framework which is inspired by vue.js, it supports two way data binding, virtual dom, directives, routing etc.
2,059 lines (1,341 loc) • 53.8 kB
JavaScript
// var event_listeners = null;
var generate_event_listeners = function (event_listeners) {
var event_listener_code = `"event_listeners": {`;
for (var type in event_listeners) {
var handlers = event_listeners[type];
event_listener_code += `"${type}": [`;
for (var i = 0; i < handlers.length; i++) {
event_listener_code += handlers[i] + ', ';
}
event_listener_code = event_listener_code.substring(0, event_listener_code.length - 2) + "], ";
}
event_listener_code = event_listener_code.substring(0, event_listener_code.length - 2) + "}, ";
return event_listener_code;
}
var generate_meta = function (meta) {
var meta_code = '{';
for (var key in meta) {
if (key == 'event_listeners') {
meta_code += generate_event_listeners(meta[key]);
} else {
meta_code += `"${key}": ${meta[key]}, `
}
}
meta_code = meta_code.substring(0, meta_code.length - 2) + '}, ';
return meta_code;
}
var default_metadata = function () {
return {should_render: false}
}
// for special directives
var empty_v_node = "m(\"#text\", " + (generate_meta(default_metadata())) + "\"\")",
special_directives = {},
directives = {};
var noop = function() {
}
var add_event_listener_code_to_v_node = function(name, handler, vnode) {
var meta = vnode.meta;
var event_listeners = meta.event_listeners;
if(event_listeners === undefined) {
event_listeners = meta.event_listeners = {};
}
var eventHandlers = event_listeners[name];
if(eventHandlers === undefined) {
event_listeners[name] = [handler];
} else {
eventHandlers.push(handler);
}
}
var event_modifiers_code = {
stop: 'event.stopPropagation();',
prevent: 'event.preventDefault();',
ctrl: 'if(event.ctrlKey === false) {return null;};',
shift: 'if(event.shiftKey === false) {return null;};',
alt: 'if(event.altKey === false) {return null;};',
enter: 'if(event.keyCode !== 13) {return null;};'
}
// ================ inbuilt directives ==============>
special_directives["m-if"] = {
after_generate: function (prop, code, vnode, state) {
var value = prop.value;
compile_template_expression(value, state.dependencies);
return (value + " ? " + code + " : " + empty_v_node);
}
}
special_directives["m-for"] = {
before_generate: function (prop, vnode, paren_v_node, state) {
// Setup Deep Flag to Flatten Array
paren_v_node.deep = true;
},
after_generate: function (prop, code, vnode, state) {
// Get dependencies
var dependencies = state.dependencies;
// Get Parts
var parts = prop.value.split(" in ");
// Aliases
var aliases = parts[0].split(",");
// The Iteratable
var iteratable = parts[1];
compile_template_expression(iteratable, dependencies);
// Get any parameters
var params = aliases.join(",");
// Add aliases to scope
for (var i = 0; i < aliases.length; i++) {
var aliasIndex = dependencies.indexOf(aliases[i]);
if (aliasIndex !== -1) {
dependencies.splice(aliasIndex, 1);
}
}
// Use the render_loop runtime helper
return ("m.render_loop(" + iteratable + ", function(" + params + ") { return " + code + "; })");
}
}
special_directives["m-on"] = {
before_generate: function (prop, vnode, paren_v_node, state) {
// Extract Event, Modifiers, and Parameters
var value = prop.value;
var meta = prop.meta;
var method_to_call = value;
var raw_modifiers = meta.args.split(".");
var event_type = raw_modifiers.shift();
var params = "event";
var raw_params = method_to_call.split("(");
if (raw_params.length > 1) {
// Custom parameters detected, update method to call, and generated parameter code
method_to_call = raw_params.shift();
params = raw_params.join("(").slice(0, -1);
compile_template_expression(params, state.dependencies);
}
// Generate any modifiers
var modifiers = "";
for (var i = 0; i < raw_modifiers.length; i++) {
var event_modifier_code = event_modifiers_code[raw_modifiers[i]];
if (event_modifier_code === undefined) {
modifiers += "if(m.render_event_modifier(event.keyCode, \"" + (raw_modifiers[i]) + "\") === false) {return null;};"
} else {
modifiers += event_modifier_code;
}
}
// Final event listener code
var code = "function(event) {" + modifiers + "instance.call_method(\"" + method_to_call + "\", [" + params + "])}";
add_event_listener_code_to_v_node(event_type, code, vnode);
}
}
special_directives["m-model"] = {
before_generate: function (prop, vnode, paren_v_node, state) {
// Get attributes
var value = prop.value;
var attrs = vnode.props.attrs;
// Get dependencies
var dependencies = state.dependencies;
console.log(' m-model :: ', value, attrs)
// Add dependencies for the getter and setter
compile_template_expression(value, dependencies);
// Setup default event type, keypath to set, value of setter, DOM property to change, and value of DOM property
var event_type = "input";
var dom_getter = "value";
var dom_setter = value;
var keypath_getter = value;
var keypath_setter = "event.target." + dom_getter;
// If input type is checkbox, listen on 'change' and change the 'checked' DOM property
var type = attrs.type;
if (type !== undefined) {
type = type.value;
var radio = false;
if (type === "checkbox" || (type === "radio" && (radio = true))) {
event_type = "change";
dom_getter = "checked";
if (radio === true) {
var value_attr = attrs.value;
var literal_value_attr = null;
var value_attrValue = "null";
if (value_attr !== undefined) {
value_attrValue = "\"" + (compile_template(value_attr.value, dependencies, true)) + "\"";
} else if ((literal_value_attr = attrs["m-literal:value"])) {
value_attrValue = "" + (compile_template(literal_value_attr.value, dependencies, true));
}
dom_setter = dom_setter + " === " + value_attrValue;
keypath_setter = value_attrValue;
} else {
keypath_setter = "event.target." + dom_getter;
}
}
}
// Compute getter base if dynamic
var bracket_index = keypath_getter.indexOf("[");
var dot_index = keypath_getter.indexOf(".");
var base = null;
var dynamic_path = null;
var dynamic_index = -1;
if (bracket_index !== -1 || dot_index !== -1) {
// Dynamic keypath found,
// Extract base and dynamic path
if (bracket_index === -1) {
dynamic_index = dot_index;
} else if (dot_index === -1) {
dynamic_index = bracket_index;
} else if (bracket_index < dot_index) {
dynamic_index = bracket_index;
} else {
dynamic_index = dot_index;
}
base = value.substring(0, dynamic_index);
dynamic_path = value.substring(dynamic_index);
// Replace string references with actual references
keypath_getter = base + dynamic_path.replace(expression_RE, function (match, reference) {
if (reference !== undefined) {
return ("\" + " + reference + " + \"");
} else {
return match;
}
});
}
// Generate the listener
var code = "function(event) {instance.set(\"" + keypath_getter + "\", " + keypath_setter + ")}";
// Push the listener to it's event listeners
add_event_listener_code_to_v_node(event_type, code, vnode);
// Setup a query used to get the value, and set the corresponding dom property
var dom = vnode.props.dom;
if (dom === undefined) {
vnode.props.dom = dom = {};
}
dom[dom_getter] = dom_setter;
}
};
special_directives["m-literal"] = {
during_prop_generate: function (prop, vnode, state) {
var prop_name = prop.meta.args;
var prop_value = prop.value;
compile_template_expression(prop_value, state.dependencies);
if (state.hasAttrs === false) {
state.hasAttrs = true;
}
if (prop_name === "class") {
// Detected class, use runtime class render helper
return ("\"class\": m.render_class(" + prop_value + "), ");
} else {
// Default literal attribute
return ("\"" + prop_name + "\": " + prop_value + ", ");
}
}
};
special_directives["m-html"] = {
before_generate: function (prop, vnode, paren_v_node, state) {
var value = prop.value;
var dom = vnode.props.dom;
if (dom === undefined) {
vnode.props.dom = dom = {};
}
compile_template_expression(value, state.dependencies);
dom.innerHTML = "(\"\" + " + value + ")";
}
}
special_directives["m-mask"] = {}
directives["m-show"] = function (el, val, vnode) {
el.style.display = (val ? '' : 'none');
}
var generate_node = function (node, parent, state) {
if (typeof node === 'string') {
var compiled = compile_template(node, state.dependencies, true); // node, dependencies, is_string
var meta = default_metadata();
if (node !== compiled) {
meta.should_render = true;
parent.meta.should_render = true;
}
return `m("#text", ${generate_meta(meta)}"${compiled}")`
} else {
var call = `m("${node.type}", `;
var meta$1 = default_metadata();
node.meta = meta$1;
var props_code = generate_props(node, parent, state);
var special_directives_after = state.special_directives_after;
if(special_directives_after !== null){
state.special_directives_after = null;
}
var children = node.children,
children_length = children.length,
children_code = '[';
if(children_length == 0){
children_code += ']';
}else{
for(var i=0; i < children_length; i++){
children_code += `${generate_node(children[i], node, state)}, `;
}
children_code = children_code.substring(0, children_code.length-2)+']';
}
if(node.deep == true){
children_code = `[].concat.apply([], ${children_code})`;
}
if(node.meta.should_render == true && parent !== undefined){
parent.meta.should_render = true;
}
call += props_code;
call += generate_meta(meta$1);
call += children_code;
call += ')';
if(special_directives_after !== null){
var special_directive_after;
for(var key in special_directives_after){
special_directive_after = special_directives_after[key];
call = special_directive_after.after_generate(special_directive_after.prop, call, node, state);
}
}
return call;
}
}
var generate_props = function (node, parent, state) {
var props = node.props;
node.props = {
attrs: props
};
var has_directives = false,
directive_props = [],
has_special_directives_after = false,
special_directives_after = {},
props_key = null,
special_directive = null;
var props_code = `{attrs: {`;
var before_generate = null;
for (props_key in props) {
var prop = props[props_key],
name = prop.name;
if ((special_directive = special_directives[name]) !== undefined && (before_generate = special_directive.before_generate) !== undefined) {
before_generate(prop, node, parent, state);
}
}
var after_generate = null,
during_prop_generate = null;
for (props_key in props) {
var prop = props[props_key],
name = prop.name;
if ((special_directive = special_directives[name]) !== undefined) {
if ((after_generate = special_directive.after_generate) !== undefined) {
special_directives_after[name] = {
prop: prop,
after_generate: after_generate
};
has_special_directives_after = true;
}
if ((during_prop_generate = special_directive.during_prop_generate) !== undefined) {
props_code += during_prop_generate(prop, node, state);
}
node.meta.should_render = true
}
else if (name[0] == 'm' && name[1] == '-') {
directive_props.push(prop);
has_directives = true;
node.meta.should_render = true
}
else {
var value = prop.value,
compiled = compile_template(value, state.dependencies, true);
if (value !== compiled) {
node.meta.should_render = true
}
if (state.has_attrs == false) {
state.has_attrs = true;
}
props_code += `"${props_key}": "${compiled}", `;
}
}
if (state.has_attrs == true) {
props_code = props_code.substring(0, props_code.length - 2) + '}';
state.has_attrs = false;
} else {
props_code += '}';
}
if(has_directives == true){
props_code += `, {directives: {`;
var directive_prop = null,
directive_prop_value = null;
for(var i=0; i <directive_props.length; i++){
directive_prop = directive_props[i];
directive_prop_value = directive_prop.value;
compile_template_expression(directive_prop_value, state.dependencies);
props_code += `"${directive_prop.name}": "${directive_prop_value.length == 0 ? "" : directive_prop_value}", `
}
props_code = props_code.substring(0, props_code.length-2)+'}'
}
if (has_special_directives_after == true) {
state.special_directives_after = special_directives_after;
}
var dom_props = node.props.dom;
if (dom_props !== undefined) {
props_code += ", dom: {";
for (var dom_prop in dom_props) {
props_code += `"${dom_prop}": ${dom_props[dom_prop]}, `;
}
props_code = props_code.substring(0, props_code.length - 2) + '}';
}
props_code += '}, ';
return props_code;
}
var compile_template = function (template, dependencies, is_string) {
var state = {
template: template,
current: 0,
dependencies: dependencies,
output: ''
}
compile_template_state(state, is_string);
return state.output;
}
var open_RE = /\{\{/,
close_RE = /\}\}/;
var compile_template_state = function (state, is_string) {
var template = state.template,
length = template.length;
while (state.current < length) {
var text_before_curelly_braces = scan_template_until(state, open_RE);
if(text_before_curelly_braces.length !== 0){
state.output += escape_string(text_before_curelly_braces);
}
if (state.current == length) {
break
}
state.current += 2; // skip opening braces;
scan_template_for_white_space(state); // skip white spaces
// if (state.current == length) {
// break
// }
var exp = scan_template_until(state, close_RE);
if (state.current == length) {
console.error(`Expected closing delimiter "}}" after "${exp}"`);
break;
}
if (exp.length !== 0) {
compile_template_expression(exp, state.dependencies);
if (is_string) {
exp = `" + ${exp} + "`;
}
state.output += exp;
}
scan_template_for_white_space(state);
state.current += 2;
}
}
var scan_template_until = function (state, RE) {
var match = "",
template = state.template,
length = template.length,
tail = template.substring(state.current);
var index = tail.search(RE);
switch (index) {
case -1:
match = tail;
break;
case 0:
match = "";
break;
default:
match = tail.substring(0, index)
}
state.current += match.length;
return match;
}
var expression_RE = /"[^"]*"|'[^']*'|\.\w*[a-zA-Z$_]\w*|\w*[a-zA-Z$_]\w*:|(\w*[a-zA-Z$_]\w*)/g;
var globals = ['true', 'false', 'undefined', 'null', 'NaN', 'typeof', 'in'];
var compile_template_expression = function (exp, dependencies) {
exp.replace(expression_RE, function (match, reference) {
if (reference !== undefined && dependencies.indexOf(reference) == -1 && globals.indexOf(reference) == -1) {
dependencies.push(reference);
}
})
return dependencies;
}
var escapeRE = /(?:(?:&(?:lt|gt|quot|amp);)|"|\\|\n)/g;
var escapeMap = {
"<": "<",
">": ">",
""": "\\\"",
"&": "&",
"\\": "\\\\",
"\"": "\\\"",
"\n": "\\n"
}
var escape_string = function (string) {
return string.replace(escapeRE, function (match) {
return escapeMap[match];
})
}
var white_space_RE = /\s/;
var scan_template_for_white_space = function (state) {
var template = state.template,
char = template[state.current];
while (white_space_RE.test(char)) {
char = template[++state.current];
}
}
var generator = function (tree) {
var root = tree.children[0];
var state = {
has_attrs: false,
dependencies: [],
special_directives_after: null
}
var root_code = generate_node(root, undefined, state); // node, parent_node, state;
var dependencies = state.dependencies;
var dependecies_code = "";
for(var i=0; i < dependencies.length; i++){
var dependency = dependencies[i];
dependecies_code += `var ${dependency} = instance.get("${dependency}"); `;
}
var code = `var instance = this; ${dependecies_code} return ${root_code};`;
console.log(' generated code is :: ', code);
try {
return new Function('m', code);
}
catch(e){
console.log(' Unable to create render function, ', e);
return noop;
}
};
var lexical_analysis = function(template){
var state = {
str: template,
tokens : [],
current : 0
};
lex_state(state);
console.log(' lex : ', state.tokens);
return state.tokens;
}
function lex_state(state){
var str = state.str,
len = state.str.length;
while(state.current < len){
// it is text
if(str.charAt(state.current) !== "<"){
get_text(state);
continue;
}
// it is comment
if(str.substr(state.current, 4) == "<!--"){
get_comment(state);
continue;
}
// its a tag
get_tag(state);
}
}
function get_text(state){
var str = state.str,
len = state.str.length,
current = state.current;
var tag_or_comment_start_RE = /<\/?(?:[A-Za-z]+\w*)|<!--/ ;
var end_of_txt = str.substring(current).search(tag_or_comment_start_RE);
if(end_of_txt == -1){
state.tokens.push({
type: 'text',
value: str.slice(current)
});
state.current = len;
return;
}else if(end_of_txt !== 0){
end_of_txt += current;
state.tokens.push({
type: 'text',
value: str.slice(current, end_of_txt)
});
state.current = end_of_txt;
}
}
function get_comment(state){
var current = state.current+4, // skip "<!--"
str = state.str,
len = state.str.length;
var end_of_comment = str.indexOf("-->", current);
if(end_of_comment == -1){
state.tokens.push({
type: 'comment',
value: str.slice(current)
});
state.current = len;
}else{
state.tokens.push({
type: 'comment',
value: str.slice(current, end_of_comment)
});
state.current = end_of_comment+3;
}
}
function get_tag(state){
var str = state.str,
len = state.str.length;
var is_tag_closing_started = str.charAt(state.current+1) == "/";
state.current += is_tag_closing_started ? 2 : 1;
var tag_token = get_tag_name(state);
get_tag_attributes(tag_token, state);
var is_tag_self_closing = str.charAt(state.current) == "/";
state.current += is_tag_self_closing ? 2 : 1;
if(is_tag_closing_started){
tag_token.tag_closing = true;
}
if(is_tag_self_closing){
tag_token.tag_self_closing = true;
}
console.log('done with get tag ');
}
function get_tag_name(state){
var current = state.current, len = state.str.length, str = state.str, tag_name = '';
while(current < len){
var char = str.charAt(current);
if(char == "/" || char == " " || char == ">"){
break;
}else{
tag_name+=char;
}
current++;
}
state.current = current;
var tag_token = {
type : 'tag',
value : tag_name
};
state.tokens.push(tag_token);
return tag_token;
}
function get_tag_attributes(tag_token, state){
var str = state.str,
len = state.str.length,
current = state.current,
char = str.charAt(current),
nextChar = str.charAt(current+1),
attributes = {};
function increment(){
current++;
char = str.charAt(current);
nextChar = str.charAt(current+1);
}
while(current < len){
if(char == ">" || (char == "/" && nextChar == ">")){
break;
}
if(char == " "){
increment();
continue;
}
var attribute_name = "", no_value_in_tag = false;
while(current < len && char !== "="){
if(char == " " || (char == "/" && nextChar == ">")){
no_value_in_tag = true;
break;
}
attribute_name+=char;
increment();
}
// skip "="
increment();
var attribute_value = {
name : attribute_name,
value : '',
meta : {}
};
console.log(attribute_name, no_value_in_tag, char);
if(no_value_in_tag){
attributes[attribute_name] = attribute_value;
continue;
}
var quote_type = "";
if( char == "'" || char == "\""){
quote_type = char;
increment();
}
while( current < len && char !== quote_type){
attribute_value.value += char;
increment();
}
console.log(' val : ', attribute_value.value, quote_type);
//skip quote end
increment();
var dot_index = attribute_name.indexOf(":");
if(dot_index !== -1){
var temp = attribute_name.split(":");
attribute_value.name = temp[0];
attribute_value.meta.args = temp[1];
}
attributes[attribute_name] = attribute_value;
}
console.log(' att ', attributes);
state.current = current;
tag_token.attributes = attributes;
}
// var str = `<p title="xk" id="this is id" m-on:click="test()">
// <!-- thsi is comment test -->
// <h1> Hello </h1>
// <span id="span_1"></span>
// <span id="span_2"></span>
// <img src="tet.img" />
// </p>
// <div id="div_test"></div>
// <div></div>`;
// lexical_analysis(str);
var __router__ = false;
var define_property = function (obj, prop, value, def) {
if (value == undefined) {
obj[prop] = def
} else {
obj[prop] = value;
}
}
var init_methods = function (instance, methods) {
var data = instance.$data;
function init_method(name, method) {
data[name] = function () {
// attache data getter and setter to the instance so they can b
instance.$data.get = instance.get.bind(instance);
instance.$data.set = instance.set.bind(instance);
// pass data to method so they can
return method.apply(instance.$data, arguments);
}
}
for (var method in methods) {
init_method(method, methods[method])
}
}
var call_hooks = function (instance, name) {
var hook = instance.$hooks[name];
if (hook !== undefined) {
hook.call(instance);
}
}
var Observer = function (instance) {
// Associated Moon Instance
this.instance = instance;
// Computed Property Cache
this.cache = {};
// Computed Property Setters
this.setters = {};
// Set of events to clear cache when dependencies change
this.clear = {};
// Property Currently Being Observed for Dependencies
this.target = null;
// Dependency Map
this.map = {};
}
var create_element = function (tag, val, props, meta, children) {
return {
type: tag,
val: val,
props: props,
children: children,
meta: meta || default_metadata()
}
}
var TEXT_TYPE = '#text', eventModifiers = {}, components = {};
/**
* Compiles Arguments to a VNode
* @param {String} tag
* @param {Object} attrs
* @param {Object} meta
* @param {Object|String} children
* @return {Object} Object usable in Virtual DOM (VNode)
*/
var m = function (tag, attrs, meta, children) {
var component = null;
if (tag == TEXT_TYPE) {
// Text Node
// Tag => #text
// Attrs => meta
// Meta => val
return create_element(TEXT_TYPE, meta, {attrs: {}}, attrs, []);
} else if ((component = components[tag]) !== undefined) {
//console.log(' found a component in \"m\" function :: ', component);
// can write code for custom compomemts
}
return create_element(tag, "", attrs, meta, children);
// In the end, we have a VNode structure like:
// {
// type: 'h1', <= nodename
// props: {
// attrs: {'id': 'someId'}, <= regular attributes
// dom: {'textContent': 'some text content'} <= only for DOM properties added by directives,
// directives: {'m-mask': ''} <= any directives
// },
// meta: {}, <= metadata used internally
// children: [], <= any child nodes
// }
}
/**
* Renders a Class in Array/Object Form
* @param {Array|Object|String} classNames
* @return {String} renderedClassNames
*/
m.render_class = function (classNames) {
if (typeof classNames === "string") {
// If they are a string, no need for any more processing
return classNames;
}
var renderedClassNames = "";
if (Array.isArray(classNames)) {
// It's an array, so go through them all and generate a string
for (var i = 0; i < classNames.length; i++) {
renderedClassNames += (m.render_class(classNames[i])) + " ";
}
} else if (typeof classNames === "object") {
// It's an object, so to through and render them to a string if the corresponding condition is truthy
for (var className in classNames) {
if (classNames[className]) {
renderedClassNames += className + " ";
}
}
}
// Remove trailing space and return
renderedClassNames = renderedClassNames.slice(0, -1);
return renderedClassNames;
}
/**
* Renders "m-for" Directive Array
* @param {Array|Object|Number} iteratable
* @param {Function} item
*/
m.render_loop = function (iteratable, item) {
var items = null;
if (Array.isArray(iteratable)) {
items = new Array(iteratable.length);
// Iterate through the array
for (var i = 0; i < iteratable.length; i++) {
items[i] = item(iteratable[i], i);
}
} else if (typeof iteratable === "object") {
items = [];
// Iterate through the object
for (var key in iteratable) {
items.push(item(iteratable[key], key));
}
} else if (typeof iteratable === "number") {
items = new Array(iteratable);
// Repeat a certain amount of times
for (var i$1 = 0; i$1 < iteratable; i$1++) {
items[i$1] = item(i$1 + 1, i$1);
}
}
return items;
}
/**
* Renders an Event Modifier
* @param {Number} keyCode
* @param {String} modifier
*/
m.render_event_modifier = function (keyCode, modifier) {
return keyCode === eventModifiers[modifier];
}
function Mini(options) {
if (options === undefined) {
options = {}
}
this.$options = options;
// save/set app name
define_property(this, '$name', options.name, 'root');
var data = options.data;
// save data
if (data == undefined) {
this.$data = {}
} else if (typeof data == 'function') {
this.$data = data();
} else {
this.$data = data;
}
// set render function if there
define_property(this, '$render', options.render, noop);
// set custom hooks
define_property(this, '$hooks', options.hooks, {});
var methods = options.methods;
if (methods !== undefined) {
init_methods(this, methods); // save methods in $data object
}
this.make_reactive(this.$data); // make objects reactive
this.$events = {};
this.$dom = {};
this.$observer = new Observer(this);
this.$destroyed = true;
this.$queued = false;
// computed method can be added here;
//=====================================
//this.init(); // initialize the app.
console.log(' checking for router :: ', __router__);
if(!__router__){
console.log(' no router found');
this.init();
}
}
var append_child = function (node, v_node, parent) {
//console.log(" appending :: ", node, v_node, parent);
parent.appendChild(node);
// can write code for custom component here, if v_node is a custom component;
}
var add_event_listeners = function (node, event_listeners) {
var add_handler = function (type) {
var handle = function (evt) {
var handlers = handle.handlers;
for (var i = 0; i < handlers.length; i++) {
handlers[i](evt);
}
}
handle.handlers = event_listeners[type]; // add handler to v_node
event_listeners[type] = handle;
node.addEventListener(type, handle); // attach event to node
}
for (var type in event_listeners) {
add_handler(type);
}
}
var diff_props = function (node, node_props, v_node, props) { // old_node, old_props, new_node, new_props
//console.log(' diff props 1 :: ', props);
var v_node_props = props.attrs;
for (var v_node_prop_name in v_node_props) {
var v_node_prop_value = v_node_props[v_node_prop_name];
var node_prop_value = node_props[v_node_prop_name];
if ((v_node_prop_value !== undefined && v_node_prop_value !== null && v_node_prop_value !== false) && (node_prop_value == undefined || node_prop_value || false && node_prop_value || null || v_node_prop_value !== node_prop_value)) {
node.setAttribute(v_node_prop_name, v_node_prop_value == true ? '' : v_node_prop_value);
}
}
// Diff Node Props with VNode Props
for (var node_prop_name in node_props) {
var v_node_prop_value$1 = v_node_props[node_prop_name];
if (v_node_prop_value$1 == undefined || v_node_prop_value$1 == false || v_node_prop_value$1 == null) {
node.removeAttribute(node_prop_name);
}
}
var v_node_directives = null; // execute directive
if ((v_node_directives = props.directives) !== undefined) {
for (var directive in v_node_directives) {
var directive_fn = null;
if ((directive_fn = v_node_directives[directive]) !== undefined) {
directive_fn(node, v_node_directives[directive], v_node);
}
}
}
var dom = null; // add/update any dom props
if ((dom = props.dom) !== undefined) {
for (var dom_prop in dom) {
var dom_prop_value = dom[dom_prop];
if (node[dom_prop] !== undefined) {
node[dom_prop] = dom_prop_value;
}
}
}
//console.log(' diff props 2 :: ', props);
}
var diff_event_listeners = function (node, new_event_listeners, old_event_listeners) {
for (var type in new_event_listeners) {
var old_event_listener = old_event_listeners[type];
if (old_event_listener == undefined) {
// if old node dont have new event listner, then remove it from the node
node.removeEventListener(type, old_event_listener); // it takes type of listener and its handler/callback function
} else {
old_event_listeners[type].handler = new_event_listeners[type];
}
}
}
var create_node_from_v_node = function (v_node) {
var type = v_node.type,
meta = v_node.meta,
el = null;
//console.log(' crreate a node: ', v_node)
if (type == '#text') {
el = document.createTextNode(v_node.val)
} else {
var children = v_node.children;
el = document.createElement(type);
var first_child = children[0];
if (children.length == 1 && first_child.type == '#text') {
el.textContent = first_child.val;
first_child.meta.el = el.firstChild;
} else {
for (var i = 0; i < children.length; i++) {
var v_child = children[i];
append_child(create_node_from_v_node(v_child), v_node, el)
}
}
var event_listeners = null; // add all event listeners;
if ((event_listeners = meta.event_listeners) !== undefined) {
add_event_listeners(el, event_listeners);
}
}
// write code for diff here tomorrow
diff_props(el, {}, v_node, v_node.props)
// hydrate
v_node.meta.el = el;
return el;
}
var replace_child = function (old_node, new_node, v_node, parent) {
var component_instance = null;
if ((component_instance = old_node._mini_) !== undefined) {
component_instance.destroy();
}
//console.log(' replacing the child', new_node, old_node);
parent.replaceChild(new_node, old_node);
// check for component;
var component = null;
if ((component = v_node.meta.component) !== undefined) {
create_node_from_v_node(new_node, v_node, component);
}
}
var remove_child = function (node, parent) {
var component_instance = null;
if ((component_instance = node._mini_) !== undefined) {
// Component was unmounted, destroy it here
component_instance.destroy();
}
parent.removeChild(node);
}
/**
* Converts attributes into key-value pairs
* @param {Node} node
* @return {Object} Key-Value pairs of Attributes
*/
var extract_attrs = function (node) {
var attrs = {};
for (var raw_attrs = node.attributes, i = raw_attrs.length; i--;) {
attrs[raw_attrs[i].name] = raw_attrs[i].value;
}
return attrs;
}
var hydrate = function (node, v_node, parent) {
var node_name = node !== null ? node.nodeName.toUpperCase() : null;
var meta = v_node.meta;
if (node_name !== v_node.type) {
var new_node = create_node_from_v_node(v_node);
replace_child(node, new_node, v_node, parent);
return new_node;
} else if (v_node == TEXT_TYPE) { // check if both are text type;
if (node.textContent !== v_node.val) {
node.textContent = v_node.val;
}
meta.el = node;
} else if (meta.component !== undefined) {
// code for component diff goes here;
} else {
meta.el = node; // hydrate
var props = v_node.props;
diff_props(node, extract_attrs(node), v_node, props);
// add event listeners
var event_listeners = null;
if ((event_listeners = meta.event_listeners) !== undefined) {
add_event_listeners(node, event_listeners);
}
// ensure inner HTML wasn't change
var dom_props = props.dom;
if (dom_props == undefined || dom_props.innerHTML == undefined) {
var children = v_node.children,
length = children.length;
var i = 0,
current_child_node = node.firstChild,
v_child = length !== 0 ? children[0] : null,
next_sibling = null;
while (v_child !== null || current_child_node !== null) {
next_sibling = null;
if (current_child_node == undefined) {
append_child(create_node_from_v_node(v_child), v_child, node);
} else {
next_sibling = current_child_node.nextSibling;
if (v_child == null) {
remove_child(current_child_node, node);
} else {
hydrate(current_child_node, v_child, node);
}
}
i++;
v_child = i < length ? children[i] : null;
current_child_node = next_sibling;
}
}
return node;
}
}
/**
* Diffs VNodes, and applies Changes
* @param {Object} oldVNode
* @param {Array} oldChildren
* @param {Object} vnode
* @param {Array} children
* @param {Number} index
* @param {Object} parent
*/
var diff = function (old_v_node, old_children, v_node, children, index, parent) {
var old_meta = old_v_node.meta;
var meta = v_node.meta;
if (old_v_node.type !== v_node.type) {
old_children[index] = v_node;
replace_child(old_meta.el, create_node_from_v_node(v_node), v_node, parent);
} else if (meta.should_render == true) {
//console.log(' didnt match in type in diff :: ', old_v_node, v_node);
if (v_node.type == TEXT_TYPE) {
var val = v_node.val;
if (old_v_node.val !== val) {
old_v_node.val = val;
old_meta.el.textContent = val;
}
} else if (meta.component !== undefined) {
//console.log(' inside diff , i found an component :: ', old_v_node, v_node);
// code for diff component will go here
} else {
var node = old_meta.el;
// diff props
var old_props = old_v_node.props;
var props = v_node.props;
diff_props(node, old_props.attrs, v_node, props);
old_props.attrs = props.attrs;
var event_listeners = null;
if ((event_listeners = meta.event_listeners) !== undefined) {
diff_event_listeners(node, event_listeners, old_meta.event_listeners);
}
// ensure html wasn't changed
var dom_props = props.dom;
if (dom_props == undefined || dom_props.innerHTML == undefined) {
// diff children;
var children$1 = v_node.children,
old_children$1 = old_v_node.children,
old_length = old_children$1.length,
new_length = children$1.length;
if (new_length == 0 && old_length !== 0) {
var first_child = null;
while ((first_child = node.firstChild) !== null) {
remove_child(first_child, node);
}
old_v_node.children = [];
} else if (old_length == 0) {
var child_v_node = null;
for (var i = 0; i < new_length; i++) {
child_v_node = children$1[i];
append_child(create_node_from_v_node(child_v_node), child_v_node, node);
}
old_v_node.children = children$1;
} else {
var total_length = new_length > old_length ? new_length : old_length;
var old_child = null,
child = null;
for (var i$1 = 0; i$1 < total_length; i$1++) {
if (i$1 >= new_length) {
// remove extra child
remove_child(old_children$1.pop().meta.el, node);
} else if (i$1 >= old_length) {
child = children$1[i$1];
append_child(create_node_from_v_node(child), child, node);
old_children$1.push(child);
} else {
// if both child don't have same reference then diff them
old_child = old_children$1[i$1];
child = children$1[i$1];
if (old_child !== child) {
diff(old_child, old_children$1, child, children$1, i$1, node);
}
}
}
}
}
//console.log(' i am done here ', v_node);
}
}
}
var hash_RE = /\[(\w+)\]/g; // replace "[", "]" with . ;
var resolve_key_path = function (instance, data, key, value) {
//console.log(' finding key path :: ', key, value, data);
key = key.replace(hash_RE, "$1");
var path = key.split('.'), temp_data = data;
var i = 0;
for (i; i < path.length - 1; i++) {
var prop_name = path[i];
data = data[prop_name];
}
//console.log(data[path[i]]);
data[path[i]] = value;
// new key may be getting added to the object so make the new key reactive
instance.make_reactive(data, value);
return path[0];
}
var queue_build = function (instance) {
if (instance.$queued === false || instance.$destroyed == false) {
instance.$queued = true;
setTimeout(function () {
instance.build();
call_hooks(instance, 'updated');
instance.$queued = false;
}, 0)
}
}
Mini.prototype.get = function (key) {
return this.$data[key];
}
Mini.prototype.set = function (key, val) {
var observer = this.$observer;
var base = resolve_key_path(this, this.$data, key, val);
//console.log(' setting the data :: ', base, this.$data);
// ******** code for components ******//
// Invoke custom setter
// var setter = null;
// if((setter = observer.setters[base]) !== undefined) {
// setter.call(this, val);
// }
//
// // Notify observer of change
// observer.notify(base, val);
// ***** ends here ********* //
queue_build(this);
}
Mini.prototype.call_method = function (method, args) {
// Get arguments
args = args || [];
args.push(this.$data);
// Call method in context of instance
return this.$data[method].apply(this, args);
}
Mini.prototype.off = function (eventName, handler) {
if (eventName === undefined) {
// No event name provided, remove all events
this.$events = {};
} else if (handler === undefined) {
// No handler provided, remove all handlers for the event name
this.$events[eventName] = [];
} else {
// Get handlers from event name
var handlers = this.$events[eventName];
// Get index of the handler to remove
var index = handlers.indexOf(handler);
// Remove the handler
handlers.splice(index, 1);
}
}
Mini.prototype.destroy = function () {
// Remove event listeners
this.off();
// Remove reference to element
this.$el = null;
// Setup destroyed state
this.$destroyed = true;
// Call destroyed hook
call_hooks(this, 'destroyed');
}
Mini.prototype.render = function () {
return this.$render(m);
}
Mini.prototype.patch = function (old, v_node, parent) {
if (old.meta !== undefined) { // check if v_node is not a v_node
//console.log(' not old : ', old, v_node);
if (old.type !== v_node.type) {
var new_root = create_node_from_v_node(v_node);
replace_child(old.meta.el, new_root, v_node, parent);
// update bounded instance
new_root._mini_ = this;
this.$el = new_root;
} else {
diff(old, [], v_node, [], 0, parent);
}
} else if (old instanceof Node) { // check old is instance of dom's Node
//console.log(' is old ', old);
var new_node = hydrate(old, v_node, parent);
if (new_node !== old) {
this.$el = v_node.meta.el;
this.$el._mini_ = this;
}
}
}
Mini.prototype.build = function () {
var dom = this.render(); // get new virtual DOM
var old = null; // old items to patch
console.log('this.$dom.meta :: ', this.$dom.meta);
if (this.$dom.meta !== undefined) { // if dom not destroyed
old = this.$dom;
} else {
old = this.$el;
this.$dom = dom;
}
console.log(' initial dom ins :: ', dom,old);
this.patch(old, dom, this.$el.parentNode)
}
Mini.compile = function (template) {
var tokens = lexical_analysis(template);
var ast = parser(tokens);
return generator(ast);
}
Mini.prototype.mount = function (el) {
this.$el = typeof el == 'string' ? document.querySelector(el) : el; // get dom element
this.$destroyed = false;
if (this.$el == null) {
//console.log(` Cannot find element "${el}"`);
}
this.$el._mini_ = this; // sync element and mini instance
console.log(' checking for template: ', this.$options.template, this.$el.outerHTML, this.$render);
define_property(this, '$template', this.$options.template, this.$el.outerHTML); // if template is given the use it else use html inside the dom element
if (this.$render === noop) {
this.$render = Mini.compile(this.$template);
}
console.log(' template is : ', this.$template);
this.build(); // run build first
call_hooks(this, 'mounted');
}
Mini.prototype.init = function () {
//console.log(' calling hooks');
call_hooks(this);
var el = this.$options.el;
if (el !== undefined) {
this.mount(el);
}
}
function parser(tokens) {
var root = {
type : 'root',
children: []
}
var state = {
current: 0,
tokens: tokens
}
while(state.current < tokens.length){
var child = getChild(state);
if(child){
root.children.push(child);
}
}
console.log(' root is :: ', root);
return root;
}
var SELF_CLOSING_ELEMENTS = ["area","base","br","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"];
var SVG_ELEMENTS = ["svg","animate","circle","clippath","cursor","defs","desc","ellipse","filter","font-face","foreignObject","g","glyph","image","line","marker","mask","missing-glyph","path","pattern","polygon","polyline","rect","switch","symbol","text","textpath","tspan","use","view"];
function createNode(node_type, props, children) {
return {
type: node_type,
props: props,
children: children
}
}
function getChild(state) {
var token = state.tokens[state.current];
var next_token = state.tokens[state.current+1];
var prev_token = state.tokens[state.current-1];
var move = function(num) {
state.current += (num == undefined ? 1 : num);
token = state.tokens[state.current];
next_token = state.tokens[state.current+1];
prev_token = state.tokens[state.current-1];
}
if(token.type == 'text'){
move();
return prev_token.value;
}
if(token.type == 'comment'){
move();
return null;
}
if(token.type == 'tag'){
var tag_type = token.value,
tag_self_closing = token.tag_self_closing,
tag_closing = token.tag_closing;
var is_svg_element = SVG_ELEMENTS.indexOf(tag_type) !== -1;
var is_self_closing_element = SELF_CLOSING_ELEMENTS.indexOf(tag_type) !== -1 || tag_self_closing == true;
var node = createNode(tag_type, token.attributes, []);
move();
if(is_svg_element){
node.is_svg = true;
}
if(is_self_closing_element){
// element is self closing so it will not have any children so no need to process further;
return node
}else if(tag_closing){
console.error(' Cannot find a closing tag for element : ', node.type);
return null;
}else if(token !== undefined){
var current = state.current;
while(token.type !== 'tag' || ((token.type == 'tag') && ((token.tag_self_closing == undefined && token.tag_closing == undefined) || (token.value !== tag_type))) ){
var child = getChild(state);