UNPKG

justice

Version:

An MVC library to make things easier.

1,033 lines (936 loc) 34.5 kB
( function( root, factory ) { if ( typeof define === "function" && define.amd ) { // AMD. Register as an anonymous module. define( [], factory ); } else if ( typeof exports === "object" ) { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { // Browser globals ( root is window ) root.returnExports = factory(); } }( this, function() { var J = makeClass(); J._i = 0; J.types = { "URL": "URL", "HTML": "HTML", "SAD": ":-(", "HAPPY": ":-)", "OK": "OK", "NO_GOOD": "NO GOOD", "JSON": "JSON", "TEXT": "TEXT", "PENDING": 0, "FULFILLED": 1, "REJECTED": 2 }; J.EventTypes = { "View.TEMPLATE_PARSED" : "View.TEMPLATE_PARSED", "URL.COMPLETE" : "URL.COMPLETE", "Mediator.ADDED" : "Mediator.ADDED" }; J._App = null; J.registerApp = function( App ) { J._App = App; }; J.getApp = function() { if ( J._App ) { return J._App; } else { throw J._makeError( "register", "Register your App with " + "J.registerApp( YourApp )", null ); } }; J.isJQueryAvailable = function() { return typeof jQuery !== "undefined"; }; J._makeError = function( id, msg, err ) { var e = new Error( msg + "\nhttps://github.com/jsz1/just/blob/master/README.md#" + id ); e.justType = id; if ( err ) { e.originalError = err; } return e; }; J.l = function() { }; J.Promise = function(fn) { var state = J.types.PENDING, value = null, handlers = []; function fulfill( result ) { state = J.types.FULFILLED; value = result; } function reject( error ) { state = J.types.REJECTED; value = error; } function resolve( result ) { try { var then = getThen( result ); if( then ) { doResolve( then.bind( result ), resolve, reject ); return; } fulfill( result ); } catch ( e ) { reject( e ); } } function getThen( value ) { var t = typeof value; if ( value && ( t === "object" || t === "function" ) ) { var then = value.then; if( typeof then === "function" ) { return then; } } return null; } function doResolve(fn, onFulfilled, onRejected) { var done = false; try { fn(function (value) { if (done){ return; } done = true; onFulfilled(value); }, function (reason) { if (done){ return; } done = true; onRejected(reason); }); } catch (ex) { if (done){ return; } done = true; onRejected(ex); } } function handle(handler) { if (state === this.types.PENDING) { handlers.push(handler); } else { if (state === this.types.FULFILLED && typeof handler.onFulfilled === "function") { handler.onFulfilled(value); } if (state === this.types.REJECTED && typeof handler.onRejected === "function") { handler.onRejected(value); } } } this.done = function (onFulfilled, onRejected) { // ensure we are always asynchronous setTimeout(function () { handle({ onFulfilled: onFulfilled, onRejected: onRejected }); }, 0); }; this.then = function (onFulfilled, onRejected) { var self = this; return new Promise(function (resolve, reject) { return self.done(function (result) { if (typeof onFulfilled === "function") { try { return resolve(onFulfilled(result)); } catch (ex) { return reject(ex); } } else { return resolve(result); } }, function (error) { if (typeof onRejected === "function") { try { return resolve(onRejected(error)); } catch (ex) { return reject(ex); } } else { return reject(error); } }); }); }; doResolve(fn, resolve, reject); }; J.url = function( url,dataType ) { return { type: J.types.URL, get: function( callback ) { var request = new XMLHttpRequest(), response; request.open( "GET", url, true ); request.onload = function() { if(typeof dataType === "undefined") { dataType = J.types.TEXT; } if ( request.status >= 200 && request.status < 400 ) { // Success! if( dataType === J.types.JSON ) { response = JSON.parse(request.responseText); } else if ( dataType === J.types.TEXT ) { response = request.responseText; } callback( { status: J.types.OK, statuz: J.types.HAPPY, statusCode: request.status, data: response } ); } else { if( dataType === J.types.JSON ) { response = JSON.parse({ error: request.responseText }); } else if ( dataType === J.types.TEXT ) { response = request.responseText; } callback( { status: J.types.NO_GOOD, statuz: J.types.SAD, statusCode: request.status, data: response } ); } }; request.onerror = function() { callback( { status: J.types.NO_GOOD, statuz: J.types.SAD, statusCode: request.status, data: request.responseText } ); }; request.send(); } }; }; J.templateCache = {}; J.isFunction = function( functionToCheck ) { var getType = {}; return functionToCheck && getType.toString.call( functionToCheck ) === "[object Function]"; }; J.html = function( html ) { return { type: "HTML", get: function( callback ) { if ( typeof callback === "undefined" ) { return html; } else if ( J.isFunction( callback ) ) { return callback( html ); } else { return html; } } }; }; J.randString = function( n ) { var text = "J", possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", i = 1; for ( i = 1; i < n; i++ ) { text += possible.charAt( Math.floor( Math.random() * possible.length ) ); } return text; }; J.Filter = { titleize: function(str) { return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); }, capitalize: function(str) { return str.charAt(0).toUpperCase() + str.slice(1); } }; J.addFilter = function(name,filter){ J.Filter[name] = filter; }; J.template = function( template, data, typeOfTemplate, callback ) { var needsCallback = true; if(J.templateCache.hasOwnProperty(template)){ needsCallback = false; } var templateNameSplit = template.split("#"), appTemplatesPath = J.getApp().config.templates; if ( typeof typeOfTemplate === "undefined" || typeOfTemplate === J.types.URL ) { // if they use trailing slashes then use trailing slashes. if(appTemplatesPath[appTemplatesPath.length-1] !== "/"){ appTemplatesPath += "/" + templateNameSplit[0]; } else { appTemplatesPath += templateNameSplit[0] + "/"; } if(needsCallback){ callback(J.compile(J.templateCache[template], templateNameSplit[1], data)); } else { J.url(appTemplatesPath).get(function(response){ J.templateCache[template] = response.data; return J.compile(response.data, templateNameSplit[1], data); }); } } else if( typeOfTemplate === J.types.HTML ) { return J.compile(this.template, templateNameSplit[1], data); } }; J.bindTemplate = function(mediator, el) { var matchers = { mediatorEvents: /on(\S)*\="{mediator\.([\S\s]+?)}"/g, mediatorInBracketsAndQuotes: /\"{([\S\s]+?)}\"/g }; var results, bracketExec, resultSplit, methodToCall, randIds = [], html = el.innerHTML; results = html.match(matchers.mediatorEvents); if(results){ for (var i = 0; i < results.length; i++) { resultSplit = results[i].split("="); html = html.replace(resultSplit[0]+"=",""); while(( bracketExec = matchers.mediatorInBracketsAndQuotes.exec(resultSplit[1])) !== null){ methodToCall = bracketExec[1].replace("mediator.",""); var randId = J.randString(9); html = html.replace(bracketExec[0],"data-just-event=\"" + randId + "\""); randIds.push({ id: randId, methodToCall: methodToCall }); } } } el.innerHTML = html; for (var j = 0; j < randIds.length; j++) { if(J.isJQueryAvailable()){ $(el).on("click","[data-just-event='" + randIds[j].id + "']", mediator._listeners[randIds[j].methodToCall]); } else { el.querySelector( "[data-just-event='" + randIds[j].id + "']" ) .addEventListener( "click", mediator._listeners[randIds[j].methodToCall]); } } }; J.compile = function( componentSource, templateName, obj ) { var matchers = { componentStart: /\*\/([\S\s]+?)\\\*/g, componentEnd: /\*\\([\S\s]+?)\/\*/g, inBetween: new RegExp("\\*\\/" + templateName + "\\\\\\*([\\S\\s]+?)\\*\\\\" + templateName + "\\/\\*", "g"), brackets: /{([\S\s]+?)}/g }; var result, toParse = "", prop, propSplit, propAndFilter, filter, skipBracket = false, filterAddon, thisObj; while ((result = matchers.componentStart.exec(componentSource)) !== null) { if(result[1] === templateName){ var inside; while ((inside = matchers.inBetween.exec(componentSource)) !== null) { toParse = inside[1]; break; } break; } } var bracketVals = toParse.match(matchers.brackets); if(!bracketVals){ return toParse; } for (var i = 0; i < bracketVals.length; i++) { while(prop = matchers.brackets.exec( bracketVals[i] )) { skipBracket = false; prop = prop[1]; propAndFilter = null; filter = null; if(prop.indexOf("|") !== -1){ propAndFilter = prop.split("|"); prop = propAndFilter[0]; filter = propAndFilter[1]; if(J.Filter.hasOwnProperty(filter)){ filter = J.Filter[filter]; } else { filter = null; } } if(prop.indexOf(".") !== -1){ // Let's grab the insides of objects by splitting them by dots. propSplit = prop.split("."); thisObj = obj; for (var j = 0; j < propSplit.length; j++) { if( thisObj.hasOwnProperty( propSplit[j] ) ){ thisObj = thisObj[propSplit[j]]; } else { // Most likely an event listener bracket. skipBracket = true; continue; } } if(skipBracket){ // Most likely an event listener bracket. continue; } if(filter){ thisObj = filter(thisObj); filterAddon = "\\|" + propAndFilter[1]; } else { filterAddon = ""; } toParse = toParse.replace(new RegExp("{" + prop + filterAddon + "}", "g"), thisObj); } else { if( obj.hasOwnProperty( prop ) ) { thisObj = obj[prop]; } else { continue; } if(filter){ thisObj = filter(thisObj); filterAddon = "\\|" + propAndFilter[1]; } else { filterAddon = ""; } toParse = toParse.replace(new RegExp("{" + prop + filterAddon + "}", "g"), obj[prop]); } } } return toParse; }; J.extends = function( SuperClass, definitionObj ) { var SubClass = makeClass(), prop; SubClass.prototype = Object.create( SuperClass.prototype ); prop = ""; if ( typeof definitionObj !== "undefined" ) { for ( prop in definitionObj ) { // first letter is uppercase if ( prop[0] === prop[0].toUpperCase() ) { // static method SubClass[prop] = definitionObj[prop]; } else { SubClass.prototype[prop] = definitionObj[prop]; } } } // Copy properties prop = ""; for ( prop in SuperClass ) { SubClass[prop] = SuperClass[prop]; } return SubClass; }; J.isObjectLiteral = function( _obj ) { var _test = _obj; return ( typeof _obj !== "object" || _obj === null ? false : ( ( function() { while ( !false ) { if ( Object.getPrototypeOf( _test = Object.getPrototypeOf( _test ) ) === null ) { break; } } return Object.getPrototypeOf( _obj ) === _test; } )() ) ); }; J.isString = function( obj ) { return typeof obj === "string"; }; J._IEVersion = null; J.IEVersion = function() { if(J._IEVersion){ return J._IEVersion; } if(document.all && !window.XMLHttpRequest){ // IE6 or older return J._IEVersion = "<=6"; } else if (document.all && !document.querySelector){ // IE7 or older return J._IEVersion = "<=7"; } else if(document.all && !document.addEventListener){ // IE8 or older return J._IEVersion = "<=8"; } else if(document.all && !window.atob){ // IE9 or older return J._IEVersion = "<=9"; } else if(document.all) { // 10 or older return J._IEVersion = "<=10"; } }; J.isIE = function(userAgent) { // Works for the IE's that we have to worry about. userAgent = userAgent || navigator.userAgent; return userAgent.indexOf("MSIE ") > -1 || userAgent.indexOf("Trident/") > -1; }; J.IEAtLeast = function(num) { // Why would anyone ever need at least version 6 of IE? I have no idea. if(num === 6){ if(J.IEVersion() === "<=6" || J.IEVersion() === "<=7" || J.IEVersion() === "<=8" || J.IEVersion() === "<=9" || J.IEVersion() === "<=10"){ return true; } } if(num === 7){ if(J.IEVersion() === "<=7" || J.IEVersion() === "<=8" || J.IEVersion() === "<=9" || J.IEVersion() === "<=10"){ return true; } } else if(num === 8){ if(J.IEVersion() === "<=8" || J.IEVersion() === "<=9" || J.IEVersion() === "<=10"){ return true; } } else if(num === 9){ if(J.IEVersion() === "<=9" || J.IEVersion() === "<=10"){ return true; } } else if(num === 10) { if(J.IEVersion() === "<=10"){ return true; } } return false; }; J.isEmpty = function( obj ) { // null and undefined are "empty" if ( obj === null ) { return true; } if ( typeof obj === "undefined" ) { return true; } // Assume if it has a length property with a non-zero value // that that property is correct. if ( obj.length > 0 ) { return false; } if ( obj.length === 0 ) { return true; } // Otherwise, does it have any properties of its own? // Note that this doesn't handle // toString and valueOf enumeration bugs in IE < 9 for ( var key in obj ) { if ( hasOwnProperty.call( obj, key ) ) { return false; } } return true; }; J.Blueprint = makeClass(); J.Blueprint.prototype.watcher = null; J.Blueprint.prototype.init = function( properties ) { this.id = ++J._i; this._properties = {}; for ( var s in properties ) { if ( s[0] === "_" ) { Object.defineProperty( this._properties, s.substring( 1 ), { value: properties[s], writable: false, enumerable: true, configurable: true } ); } else { this._properties[s] = properties[s]; } } }; J.Blueprint.prototype._super = function(SuperClass,toCall,params){ SuperClass.prototype[toCall].apply(this,params); }; J.Blueprint.prototype.get = function( arg ) { return this._properties[arg]; }; J.Blueprint.prototype.set = function( key, val ) { var oldVal = this._properties[key]; if ( this._properties[key] !== val ) { if ( J.isString( key ) && key[0] === "_" ) { Object.defineProperty( this._properties, key.substring( 1 ), { value: val, writable: false, enumerable: true, configurable: true } ); } else { this._properties[key] = val; } if ( this.watcher ) { this.watcher.call( this, { key:key, val:val, oldVal:oldVal } ); } } }; J.App = makeClass(); J.App.prototype.init = function() { this.events = J.Events(); this.views = {}; this.mediators = {}; this.config = { root: document.URL, template: document.URL + "/templates" }; this.templateCache = {}; }; J.App.prototype.addView = function( name, view ) { this.views[name] = view; return view; }; J.App.prototype.addMediator = function( view, mediator ) { this.mediators[view] = mediator; this.views[view].mediator = mediator; J._App.events.trigger(this.views[view].el, J.EventTypes["Mediator.ADDED"], { mediator: mediator, view: view }); return mediator; }; J.App.prototype.config = function( configObj ) { if(configObj !== "undefined"){ this.config = configObj; } else { return this.config; } }; J.App.prototype.getView = function( name ) { return this.views[name]; }; J.App.prototype.getAllViews = function() { return this.views; }; J.App.prototype.removeView = function( name ) { delete this.views[name]; return name; }; J.Events = makeClass(); J.Events.prototype.init = function() { }; // http://stackoverflow.com/questions/5342917 J.Events.prototype.trigger = function( el, eventName, data ) { var event; if ( window.document.createEvent ) { if(typeof data !== "undefined") { if(J.isJQueryAvailable()){ $(el).trigger(eventName, data); return; } if( J.IEAtLeast(9) || J.isIE() ){ event = window.document.createEvent(eventName); event.initCustomEvent(eventName, false, false, data); } else { event = new CustomEvent(eventName, { detail: data }); el.dispatchEvent(event); } } else { if(J.isJQueryAvailable()){ $(el).trigger(eventName); return; } else { event = document.createEvent( "HTMLEvents" ); event.initEvent( eventName, true, true ); } } }else if ( document.createEventObject ) {// IE < 9 event = document.createEventObject(); event.eventType = eventName; } event.eventName = eventName; if(typeof el === "undefined"){ return; } if ( el.dispatchEvent ) { el.dispatchEvent( event ); }else if ( el.fireEvent ) {// IE < 9 el.fireEvent( "on" + event.eventType, event ); }else if ( el[eventName] ) { el[eventName](); }else if ( el["on" + eventName] ) { el["on" + eventName](); } return el; }; J.Events.prototype.add = function( el, type, handler ) { if(J.isJQueryAvailable()){ $(el).on(type, handler); return; } if ( el.addEventListener ) { el.addEventListener( type, handler, false ); }else if ( el.attachEvent ) {// IE < 9 if(J.isJQueryAvailable()){ $(el).on(type, handler); return; } el.attachEvent( "on" + type, handler ); }else { el["on" + type] = handler; } return el; }; J.Events.prototype.remove = function( el, type, handler ) { if ( el.removeEventListener ) { el.removeEventListener( type, handler, false ); }else if ( el.detachEvent ) {// IE < 9 if(J.isJQueryAvailable()){ $(el).off(type, handler); return; } else { el.detachEvent( "on" + type, handler ); } }else { el["on" + type] = null; } return el; }; // Model J.Model = makeClass(); J.Model.prototype = Object.create( J.Blueprint.prototype ); J.Model.prototype.init = function( properties ) { J.Blueprint.prototype.init.call( this, properties ); }; J.Model.prototype.toJSON = function() { return JSON.stringify( this._properties ); }; J.Model.prototype.toObject = function() { return this._properties; }; J.View = makeClass(); J.View.prototype = Object.create( J.Blueprint.prototype ); J.View.prototype.init = function( properties ) { J.Blueprint.prototype.init.call( this ); if ( typeof properties !== "undefined" ) { for ( var s in properties ) { if ( s === "template" ) { this.template = properties[s]; } else if ( s === "bind" ) { this.bindables = properties[s]; } else if ( s === "el" ) { this.el = properties[s]; } } } }; J.View.prototype.initBindables = function() { var newSelector, newId, bindablesAttributes, i = 0; this._bindableMappings = []; bindablesAttributes = this.el.querySelectorAll( "[data-j-bindable]" ); for ( i = 0; i < bindablesAttributes.length; i++ ) { newId = J.randString( 5 ); bindablesAttributes[i].setAttribute( "data-j-bound-id", newId ); newSelector = bindablesAttributes[i].tagName + "[data-j-bound-id='" + newId + "']"; this._bindableMappings.push( { prop: bindablesAttributes[i].getAttribute( "data-j-bindable" ), element: newSelector } ); bindablesAttributes[i].removeAttribute( "data-j-bindable" ); } }; J.View.prototype.initBindableListeners = function() { var actionTarget, action, target, i = 0; for ( i = 0; i < this._bindableMappings.length; i++ ) { if ( this.bindables.hasOwnProperty( this._bindableMappings[i].prop ) ) { actionTarget = this.bindables[this._bindableMappings[i].prop]; action = actionTarget.substring( 0, actionTarget.indexOf( " " ) ); target = actionTarget.substring( actionTarget.indexOf( " " ) + 1 ); addEventToTarget( this, i ); } } function addEventToTarget( that, i ) { that.el.querySelector( target ).addEventListener( action, function() { that.el.querySelector( that._bindableMappings[i].element ) .innerText = this.value; } ); } }; J.View.prototype.template = function( templateObj ) { if ( typeof templateObj !== "undefined" ) { this.template = templateObj; } else { return this.template; } }; J.View.prototype.prerender = function(typeOfTemplate, callback) { if(J.templateCache.hasOwnProperty(this.template)){ return J.templateCache[this.template]; } else { return null; } var templateNameSplit = this.template.split("#"), appTemplatesPath = J.getApp().config.templates; if(typeof typeOfTemplate === "undefined" || typeOfTemplate === null || typeOfTemplate === J.types.URL){ typeOfTemplate = J.types.URL; } if ( typeof typeOfTemplate === "undefined" || typeOfTemplate === J.types.URL ) { // if they use trailing slashes then use trailing slashes. if(appTemplatesPath[appTemplatesPath.length-1] !== "/"){ appTemplatesPath += "/" + templateNameSplit[0]; } else { appTemplatesPath += templateNameSplit[0] + "/"; } J.url(appTemplatesPath) .get(function(response){ J.templateCache[this.template] = response.data; J._App.events.trigger(this.el, "View.TEMPLATE_PRERENDERED"); if(typeof callback !== "undefined"){ callback(); } }.bind(this)); } }; J.View.prototype.render = function( data, typeOfTemplate, callback ) { var renderedHTML; if(typeof typeOfTemplate === "undefined" || typeOfTemplate === null){ typeOfTemplate = J.types.URL; } if(!this.template){ throw J._makeError( "template_missing", "View is missing a template", null ); } var needsCallback = true; if(J.templateCache.hasOwnProperty(this.template)){ needsCallback = false; } J.template(this.template, data, typeOfTemplate, function(rendered){ renderedHTML = rendered; this.el.innerHTML = renderedHTML; // bind the events on this element to this mediator J.bindTemplate(this.mediator, this.el); J._App.events.trigger(this.el, "View.TEMPLATE_PARSED",{ data: renderedHTML }); if(typeof callback !== "undefined"){ callback(renderedHTML); } }.bind(this)); if(!needsCallback){ J.bindTemplate(this.mediator, this.el); return renderedHTML; } else { return null; } }; J.Mediator = makeClass(); J.Mediator.prototype = Object.create( J.Blueprint.prototype ); J.Mediator.prototype.init = function( properties ) { this._listeners = {}; this._events = {}; if ( typeof properties !== "undefined" ) { for ( var s in properties ) { if ( s === "view" ) { this.view = properties[s]; this.view.mediator = this; } else if ( s === "events" ) { this._events = properties[s]; } else if ( s === "listeners" ) { this._listeners = properties[s]; } } if ( !J.isEmpty( this._events ) && !J.isEmpty( this._listeners ) ) { this.events.call( this ); } if( !J.isEmpty( this._listeners ) ) {} } }; J.Mediator.prototype.events = function() { var actionTarget, action, target, query, missingEvents = [], s = ""; if ( typeof this._events !== "undefined" ) { missingEvents = []; for ( s in this._events ) { if ( this._listeners.hasOwnProperty( this._events[s] ) ) { actionTarget = s; action = actionTarget.substring( 0, actionTarget.indexOf( " " ) ); target = actionTarget.substring( actionTarget.indexOf( " " ) + 1 ); if(J.isJQueryAvailable()){ query = $(target); } else { query = this.view.el.querySelectorAll( target ); } if ( query ) { if(J.isJQueryAvailable()){ query.on(action, this._listeners[this._events[s]]); } else { for (var i = 0; i < query.length; i++) { query[i].addEventListener( action, this._listeners[this._events[s]] ); } } } else { missingEvents.push( { action: query } ); } // } } } }; J.Mapper = makeClass(); J.Mapper.prototype = Object.create( J.Blueprint ); J.Mapper.prototype.viewMap = {}; J.Mapper.prototype.singleModelMap = {}; J.Mapper.prototype.modelMap = {}; J.Mapper.prototype.mapView = function( name, view, mediator ) { this.viewMap[name] = { "view": view, "mediator": mediator }; // this.viewMap.push( singleMap ); return this.viewMap[name]; }; J.Mapper.prototype.mapAModel = function( name, Model, properties ) { var p = typeof properties === undefined ? {} : properties; this.singleModelMap[name] = Model( p ); return this.singleModelMap[name]; }; J.Mapper.prototype.getAModel = function( name ) { return this.singleModelMap[name]; }; J.Mapper.prototype.mapModel = function( name, Model ) { this.modelMap[name] = Model; return this.modelMap[name]; }; J.Mapper.prototype.getModel = function( name, properties ) { var p = typeof properties === undefined ? {} : properties; return this.modelMap[name]( p ); }; function makeClass() { return function F( args ) { if ( this instanceof F ) { if ( typeof this.init === "function" ) { this.init.apply( this, args ); } } else { return new F( arguments ); } }; } window.J = J; return J; } ) );