forerunnerdb
Version:
A NoSQL document store database for browsers and Node.js.
1,431 lines (1,248 loc) • 233 kB
JavaScript
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
var AutoBind = _dereq_('../lib/AutoBind');
module.exports = AutoBind;
},{"../lib/AutoBind":2}],2:[function(_dereq_,module,exports){
"use strict";
/**
* Provides data-binding functionality to ForerunnerDB. Allows collections
* and views to link to selectors and automatically generate DOM elements
* from jsViews (jsRender) templates.
*/
var Shared = window.ForerunnerDB.shared,
AutoBind = {},
jsviews;
Shared.addModule('AutoBind', AutoBind);
AutoBind.extendCollection = function (Module) {
var superInit = Module.prototype.init,
superDataReplace = Module.prototype._dataReplace,
superDataInsertIndex = Module.prototype._dataInsertAtIndex,
superDataRemoveIndex = Module.prototype._dataRemoveAtIndex,
superUpdateProperty = Module.prototype._updateProperty,
superUpdateIncrement = Module.prototype._updateIncrement,
superUpdateSpliceMove = Module.prototype._updateSpliceMove,
superUpdateSplicePush = Module.prototype._updateSplicePush,
superUpdatePush = Module.prototype._updatePush,
superUpdatePull = Module.prototype._updatePull,
superUpdateMultiply = Module.prototype._updateMultiply,
superUpdateRename = Module.prototype._updateRename,
superUpdateOverwrite = Module.prototype._updateOverwrite,
superUpdateUnset = Module.prototype._updateUnset,
superUpdatePop = Module.prototype._updatePop,
superDrop = Module.prototype.drop;
Module.prototype.init = function () {
this._linked = 0;
superInit.apply(this, arguments);
};
Module.prototype.isLinked = function () {
return Boolean(this._linked);
};
/**
* Creates a link to the DOM between the collection data and the elements
* in the passed output selector. When new elements are needed or changes
* occur the passed templateSelector is used to get the template that is
* output to the DOM.
* @param outputTargetSelector
* @param templateSelector
* @param {Object=} options Optional extra options.
*/
Module.prototype.link = function (outputTargetSelector, templateSelector, options) {
if (window.jQuery) {
// Make sure we have a data-binding store object to use
this._links = this._links || {};
var templateId,
templateHtml;
if (templateSelector && typeof templateSelector === 'object') {
// Our second argument is an object, let's inspect
if (templateSelector.template && typeof templateSelector.template === 'string') {
// The template has been given to us as a string
templateId = this.objectId(templateSelector.template);
templateHtml = templateSelector.template;
}
} else {
templateId = templateSelector;
}
if (!this._links[templateId]) {
if (window.jQuery(outputTargetSelector).length) {
// Ensure the template is in memory and if not, try to get it
if (!window.jQuery.templates[templateId]) {
if (!templateHtml) {
// Grab the template
var template = window.jQuery(templateSelector);
if (template.length) {
templateHtml = window.jQuery(template[0]).html();
} else {
throw('ForerunnerDB.AutoBind "' + this.name() + '": Unable to bind collection to target because template "' + templateSelector + '" does not exist');
}
}
window.jQuery.views.templates(templateId, templateHtml);
}
if (options && options.$wrap) {
var wrapper,
tmpObj,
doc;
if (!options.$wrapIn) {
// Create the data binding wrapped in an object
wrapper = {};
wrapper[options.$wrap] = this._data;
} else if (options.$wrapIn instanceof window.ForerunnerDB.shared.modules.Document) {
// Document-based wrapper
// Grab the document instance
doc = options.$wrapIn;
// Get the current data by reference
tmpObj = doc._data;
// Set the wrapper property to the referenced data
// of this collection / view
tmpObj[options.$wrap] = this._data;
// Set the data back into the document by reference
doc.setData(tmpObj, {$decouple: false});
// Set it to data-bound mode
doc._linked = 1;
// Provide the document data as wrapper data
wrapper = doc._data;
}
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Binding with data wrapper "' + options.$wrap + '" for collection "' + this.name() + '" to output target: ' + outputTargetSelector);
}
window.jQuery.templates[templateId].link(outputTargetSelector, wrapper);
} else {
// Create the data binding
window.jQuery.templates[templateId].link(outputTargetSelector, this._data);
}
// Add link to flags
this._links[templateId] = outputTargetSelector;
// Set the linked flag
this._linked++;
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Added binding collection "' + this.name() + '" to output target: ' + outputTargetSelector);
}
return this;
} else {
throw('ForerunnerDB.AutoBind "' + this.name() + '": Cannot bind collection to target selector "' + outputTargetSelector + '" because it does not exist in the DOM!');
}
}
throw('ForerunnerDB.AutoBind "' + this.name() + '": Attempt to bind a duplicate link from collection to the target: ' + outputTargetSelector + ' with the template: ' + templateId);
} else {
throw('ForerunnerDB.AutoBind "' + this.name() + '": Cannot data-bind without jQuery. Please add jQuery to your page!');
}
};
/**
* Removes a link to the DOM between the collection data and the elements
* in the passed output selector that was created using the link() method.
* @param outputTargetSelector
* @param templateSelector
*/
Module.prototype.unlink = function (outputTargetSelector, templateSelector) {
if (window.jQuery) {
var templateId,
i;
// Check for binding
this._links = this._links || {};
if (outputTargetSelector && templateSelector) {
if (templateSelector && typeof templateSelector === 'object') {
// Our second argument is an object, let's inspect
if (templateSelector.template && typeof templateSelector.template === 'string') {
// The template has been given to us as a string
templateId = this.objectId(templateSelector.template);
}
} else {
templateId = templateSelector;
}
if (this._links[templateId]) {
// Remove the data binding
window.jQuery.templates[templateId].unlink(outputTargetSelector);
// Remove link from flags
delete this._links[templateId];
// Set the linked flag
this._linked--;
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Removed binding collection "' + this.name() + '" to output target: ' + outputTargetSelector);
}
return this;
}
if (this.debug()) {
console.log('ForerunnerDB.AutoBind "' + this.name() + '": Cannot remove link from collection, one does not exist to the target: ' + outputTargetSelector + ' with the template: ' + templateSelector);
}
} else {
// No parameters passed, unlink all from this module
for (i in this._links) {
if (this._links.hasOwnProperty(i)) {
window.jQuery.templates[i].unlink(this._links[i]);
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Removed binding collection "' + this.name() + '" to output target: ' + this._links[i]);
}
}
}
this._links = {};
this._linked = 0;
}
} else {
throw('ForerunnerDB.AutoBind "' + this.name() + '": Cannot data-bind without jQuery. Please add jQuery to your page!');
}
return this;
};
Module.prototype._dataReplace = function (data) {
if (this._linked) {
// Remove all items
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Replacing some data in document for collection "' + this.name() + '"');
}
window.jQuery.observable(this._data).refresh(data);
} else {
superDataReplace.apply(this, arguments);
}
};
Module.prototype._dataInsertAtIndex = function (index, doc) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Inserting some data for collection "' + this.name() + '"');
}
window.jQuery.observable(this._data).insert(index, doc);
} else {
superDataInsertIndex.apply(this, arguments);
}
};
Module.prototype._dataRemoveAtIndex = function (index) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Removing some data for collection "' + this.name() + '"');
}
window.jQuery.observable(this._data).remove(index);
} else {
superDataRemoveIndex.apply(this, arguments);
}
};
/**
* Updates a property on an object depending on if the collection is
* currently running data-binding or not.
* @param {Object} doc The object whose property is to be updated.
* @param {String} prop The property to update.
* @param {*} val The new value of the property.
* @private
*/
Module.prototype._updateProperty = function (doc, prop, val) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Setting document property "' + prop + '" for collection "' + this.name() + '"');
}
window.jQuery.observable(doc).setProperty(prop, val);
} else {
superUpdateProperty.apply(this, arguments);
}
};
/**
* Increments a value for a property on a document by the passed number.
* @param {Object} doc The document to modify.
* @param {String} prop The property to modify.
* @param {Number} val The amount to increment by.
* @private
*/
Module.prototype._updateIncrement = function (doc, prop, val) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Incrementing document property "' + prop + '" for collection "' + this.name() + '"');
}
window.jQuery.observable(doc).setProperty(prop, doc[prop] + val);
} else {
superUpdateIncrement.apply(this, arguments);
}
};
/**
* Changes the index of an item in the passed array.
* @param {Array} arr The array to modify.
* @param {Number} indexFrom The index to move the item from.
* @param {Number} indexTo The index to move the item to.
* @private
*/
Module.prototype._updateSpliceMove = function (arr, indexFrom, indexTo) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Moving document array index from "' + indexFrom + '" to "' + indexTo + '" for collection "' + this.name() + '"');
}
window.jQuery.observable(arr).move(indexFrom, indexTo);
} else {
superUpdateSpliceMove.apply(this, arguments);
}
};
/**
* Inserts an item into the passed array at the specified index.
* @param {Array} arr The array to insert into.
* @param {Number} index The index to insert at.
* @param {Object} doc The document to insert.
* @private
*/
Module.prototype._updateSplicePush = function (arr, index, doc) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Pushing item into document sub-array for collection "' + this.name() + '"');
}
if (arr.length > index) {
window.jQuery.observable(arr).insert(index, doc);
} else {
window.jQuery.observable(arr).insert(doc);
}
} else {
superUpdateSplicePush.apply(this, arguments);
}
};
/**
* Inserts an item at the end of an array.
* @param {Array} arr The array to insert the item into.
* @param {Object} doc The document to insert.
* @private
*/
Module.prototype._updatePush = function (arr, doc) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Pushing item into document sub-array for collection "' + this.name() + '"');
}
window.jQuery.observable(arr).insert(doc);
} else {
superUpdatePush.apply(this, arguments);
}
};
/**
* Removes an item from the passed array.
* @param {Array} arr The array to modify.
* @param {Number} index The index of the item in the array to remove.
* @private
*/
Module.prototype._updatePull = function (arr, index) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Pulling item from document sub-array for collection "' + this.name() + '"');
}
window.jQuery.observable(arr).remove(index);
} else {
superUpdatePull.apply(this, arguments);
}
};
/**
* Multiplies a value for a property on a document by the passed number.
* @param {Object} doc The document to modify.
* @param {String} prop The property to modify.
* @param {Number} val The amount to multiply by.
* @private
*/
Module.prototype._updateMultiply = function (doc, prop, val) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Multiplying value for collection "' + this.name() + '"');
}
window.jQuery.observable(doc).setProperty(prop, doc[prop] * val);
} else {
superUpdateMultiply.apply(this, arguments);
}
};
/**
* Renames a property on a document to the passed property.
* @param {Object} doc The document to modify.
* @param {String} prop The property to rename.
* @param {Number} val The new property name.
* @private
*/
Module.prototype._updateRename = function (doc, prop, val) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Renaming property "' + prop + '" to "' + val + '" on document for collection "' + this.name() + '"');
}
window.jQuery.observable(doc).setProperty(val, doc[prop]);
window.jQuery.observable(doc).removeProperty(prop);
} else {
superUpdateRename.apply(this, arguments);
}
};
/**
* Overwrites a property on a document to the passed value.
* @param {Object} doc The document to modify.
* @param {String} prop The property to delete.
* @param {*} val The new value to set the property to.
* @private
*/
Module.prototype._updateOverwrite = function (doc, prop, val) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Setting document property "' + prop + '" for collection "' + this.name() + '"');
}
window.jQuery.observable(doc).setProperty(prop, val);
} else {
superUpdateOverwrite.apply(this, arguments);
}
};
/**
* Deletes a property on a document.
* @param {Object} doc The document to modify.
* @param {String} prop The property to delete.
* @private
*/
Module.prototype._updateUnset = function (doc, prop) {
if (this._linked) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Removing property "' + prop + '" from document for collection "' + this.name() + '"');
}
window.jQuery.observable(doc).removeProperty(prop);
} else {
superUpdateUnset.apply(this, arguments);
}
};
/**
* Pops an item from the array stack.
* @param {Object} doc The document to modify.
* @param {Number=} val Optional, if set to 1 will pop, if set to -1 will shift.
* @return {Boolean}
* @private
*/
Module.prototype._updatePop = function (doc, val) {
var index,
updated = false;
if (this._linked) {
if (doc.length > 0) {
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Popping item from sub-array in document for collection "' + this.name() + '"');
}
if (val === 1) {
index = doc.length - 1;
} else if (val === -1) {
index = 0;
}
if (index > -1) {
window.jQuery.observable(doc).remove(index);
updated = true;
}
}
} else {
updated = superUpdatePop.apply(this, arguments);
}
return updated;
};
Module.prototype.drop = function () {
// Unlink all linked data
var i;
if (this._linked) {
for (i in this._links) {
if (this._links.hasOwnProperty(i)) {
this.unlink(this._links[i], i);
}
}
}
return superDrop.apply(this, arguments);
};
};
AutoBind.extendView = function (Module) {
var superInit = Module.prototype.init;
Module.prototype.init = function () {
this._linked = 0;
superInit.apply(this, arguments);
};
Module.prototype.isLinked = function () {
return this.publicData().isLinked();
};
/**
* Data-binds the view data to the elements matched by the passed selector.
* @param {String} outputTargetSelector The jQuery element selector to select the element
* into which the data-bound rendered items will be placed. All existing HTML will be
* removed from this element.
* @param {String|Object} templateSelector This can either be a jQuery selector identifying
* which template element to get the template HTML from that each item in the view's data
* will use when rendering to the screen, or you can pass an object with a template key
* containing a string that represents the HTML template such as:
* { template: '<div>{{:name}}</div>' }
* @param {Object=} options An options object.
* @returns {*}
*/
Module.prototype.link = function (outputTargetSelector, templateSelector, options) {
var publicData = this.publicData();
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Setting up data binding on view "' + this.name() + '" in underlying (internal) view collection "' + publicData.name() + '" for output target: ' + outputTargetSelector);
}
publicData.link(outputTargetSelector, templateSelector, options);
return this;
};
Module.prototype.unlink = function (outputTargetSelector, templateSelector) {
var publicData = this.publicData();
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Removing data binding on view "' + this.name() + '" in underlying (internal) view collection "' + publicData.name() + '" for output target: ' + outputTargetSelector);
}
publicData.unlink(outputTargetSelector, templateSelector);
return this;
};
};
AutoBind.extendOverview = function (Module) {
Module.prototype.isLinked = function () {
return this.data().isLinked();
};
/**
* Creates a link to the DOM between the overview data and the elements
* in the passed output selector. When new elements are needed or changes
* occur the passed templateSelector is used to get the template that is
* output to the DOM.
* @param outputTargetSelector
* @param templateSelector
* @param {Object=} options An options object.
*/
Module.prototype.link = function (outputTargetSelector, templateSelector, options) {
this._data.link.apply(this._data, arguments);
this._refresh();
};
/**
* Removes a link to the DOM between the overview data and the elements
* in the passed output selector that was created using the link() method.
* @param outputTargetSelector
* @param templateSelector
*/
Module.prototype.unlink = function (outputTargetSelector, templateSelector) {
this._data.unlink.apply(this._data, arguments);
this._refresh();
};
};
AutoBind.extendDocument = function (Module) {
Module.prototype.isLinked = function () {
return Boolean(this._linked);
};
/**
* Creates a link to the DOM between the document data and the elements
* in the passed output selector. When new elements are needed or changes
* occur the passed templateSelector is used to get the template that is
* output to the DOM.
* @param outputTargetSelector
* @param templateSelector
* @param {Object=} options An options object.
*/
Module.prototype.link = function (outputTargetSelector, templateSelector, options) {
if (window.jQuery) {
// Make sure we have a data-binding store object to use
this._links = this._links || {};
if (!this._linked) { this._linked = 0; }
var templateId,
templateHtml;
if (templateSelector && typeof templateSelector === 'object') {
// Our second argument is an object, let's inspect
if (templateSelector.template && typeof templateSelector.template === 'string') {
// The template has been given to us as a string
templateId = this.objectId(templateSelector.template);
templateHtml = templateSelector.template;
}
} else {
templateId = templateSelector;
}
if (!this._links[templateId]) {
if (window.jQuery(outputTargetSelector).length) {
// Ensure the template is in memory and if not, try to get it
if (!window.jQuery.templates[templateId]) {
if (!templateHtml) {
// Grab the template
var template = window.jQuery(templateSelector);
if (template.length) {
templateHtml = window.jQuery(template[0]).html();
} else {
throw('ForerunnerDB.AutoBind "' + this.name() + '": Unable to bind document to target because template does not exist: ' + templateSelector);
}
}
window.jQuery.views.templates(templateId, templateHtml);
}
if (options && options.$wrap) {
// Create the data binding wrapped in an object
var wrapper,
tmpObj,
doc;
if (!options.$wrapIn) {
// Create the data binding wrapped in an object
wrapper = {};
wrapper[options.$wrap] = this._data;
} else if (options.$wrapIn instanceof Document) {
// Document-based wrapper
// Grab the document instance
doc = options.$wrapIn;
// Get the current data by reference
tmpObj = doc._data;
// Set the wrapper property to the referenced data
// of this collection / view
tmpObj[options.$wrap] = this._data;
// Set the data back into the document by reference
doc.setData(tmpObj, {$decouple: false});
// Set it to data-bound mode
doc._linked = 1;
// Provide the document data as wrapper data
wrapper = options.$wrap._data;
}
window.jQuery.templates[templateId].link(outputTargetSelector, wrapper);
} else {
// Create the data binding
window.jQuery.templates[templateId].link(outputTargetSelector, this._data);
}
// Add link to flags
this._links[templateId] = outputTargetSelector;
// Set the linked flag
this._linked++;
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Added binding document "' + this.name() + '" to target: ' + outputTargetSelector);
}
return this;
} else {
throw('ForerunnerDB.AutoBind "' + this.name() + '": Cannot bind document to target "' + outputTargetSelector + '" because it does not exist in the DOM!');
}
}
throw('ForerunnerDB.AutoBind "' + this.name() + '": Cannot create a duplicate link from document to the target: ' + outputTargetSelector + ' with the template: ' + templateId);
} else {
throw('ForerunnerDB.AutoBind "' + this.name() + '": Cannot data-bind without jQuery. Please add jQuery to your page!');
}
};
/**
* Removes a link to the DOM between the document data and the elements
* in the passed output selector that was created using the link() method.
* @param outputTargetSelector
* @param templateSelector
*/
Module.prototype.unlink = function (outputTargetSelector, templateSelector) {
if (window.jQuery) {
// Check for binding
this._links = this._links || {};
var templateId,
i;
if (outputTargetSelector && templateSelector) {
if (templateSelector && typeof templateSelector === 'object') {
// Our second argument is an object, let's inspect
if (templateSelector.template && typeof templateSelector.template === 'string') {
// The template has been given to us as a string
templateId = this.objectId(templateSelector.template);
}
} else {
templateId = templateSelector;
}
if (this._links[templateId]) {
// Remove the data binding
window.jQuery.templates[templateId].unlink(outputTargetSelector);
// Remove link from flags
delete this._links[templateId];
// Set the linked flag
this._linked--;
if (this.debug()) {
console.log('ForerunnerDB.AutoBind "' + this.name() + '": Removed binding document to target: ' + outputTargetSelector);
}
return this;
}
if (this.debug()) {
console.log('ForerunnerDB.AutoBind "' + this.name() + '": Cannot remove link from document, one does not exist to the target: ' + outputTargetSelector + ' with the template: ' + templateSelector);
}
} else {
// No parameters passed, unlink all from this module
for (i in this._links) {
if (this._links.hasOwnProperty(i)) {
window.jQuery.templates[i].unlink(this._links[i]);
if (this.debug()) {
console.log('ForerunnerDB.AutoBind: Removed binding document "' + this.name() + '" to output target: ' + this._links[i]);
}
}
}
this._links = {};
this._linked = 0;
}
} else {
throw('ForerunnerDB.AutoBind "' + this.name() + '": Cannot data-bind without jQuery. Please add jQuery to your page!');
}
};
};
// Check that jQuery exists before doing anything else
if (typeof window.jQuery !== 'undefined') {
// Load jsViews
jsviews = _dereq_('../lib/vendor/jsviews');
// Ensure jsviews is registered
if (typeof window.jQuery.views !== 'undefined') {
// Define modules that we wish to work on
var modules = ['Collection', 'View', 'Overview', 'Document'],
moduleIndex,
moduleFinished = function (name, module) {
if (AutoBind['extend' + name]) {
AutoBind['extend' + name](module);
}
};
// Extend modules that are finished loading
for (moduleIndex = 0; moduleIndex < modules.length; moduleIndex++) {
Shared.moduleFinished(modules[moduleIndex], moduleFinished);
}
Shared.finishModule('AutoBind');
} else {
throw('ForerunnerDB.AutoBind : Plugin cannot continue because jsViews is not loaded. Check your error log for url errors; it should have automatically loaded with this plugin.');
}
} else {
throw('ForerunnerDB.AutoBind : Cannot data-bind without jQuery. Please add jQuery to your page!');
}
Shared.finishModule('AutoBind');
module.exports = AutoBind;
},{"../lib/vendor/jsviews":3}],3:[function(_dereq_,module,exports){
var init = (function () {
/*! jsviews.js v1.0.0-alpha single-file version:
includes JsRender, JsObservable and JsViews http://github.com/BorisMoore/jsrender and http://jsviews.com/jsviews
informal pre V1.0 commit counter: 60 (Beta Candidate) */
/* JsRender:
* See http://github.com/BorisMoore/jsrender and http://jsviews.com/jsrender
* Copyright 2014, Boris Moore
* Released under the MIT License.
*/
(function(global, jQuery, undefined) {
// global is the this object, which is window when running in the usual browser environment.
"use strict";
if (jQuery && jQuery.render || global.jsviews) { return; } // JsRender is already loaded
//========================== Top-level vars ==========================
var versionNumber = "v1.0.0-beta",
$, jsvStoreName, rTag, rTmplString, indexStr, // nodeJsModule,
//TODO tmplFnsCache = {},
delimOpenChar0 = "{", delimOpenChar1 = "{", delimCloseChar0 = "}", delimCloseChar1 = "}", linkChar = "^",
rPath = /^(!*?)(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g,
// none object helper view viewProperty pathTokens leafToken
rParams = /(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*\.|\s*\^|\s*$)|[)\]])([([]?))|(\s+)/g,
// lftPrn0 lftPrn bound path operator err eq path2 prn comma lftPrn2 apos quot rtPrn rtPrnDot prn2 space
// (left paren? followed by (path? followed by operator) or (path followed by left paren?)) or comma or apos or quot or right paren or space
rNewLine = /[ \t]*(\r\n|\n|\r)/g,
rUnescapeQuotes = /\\(['"])/g,
rEscapeQuotes = /['"\\]/g, // Escape quotes and \ character
rBuildHash = /(?:\x08|^)(onerror:)?(?:(~?)(([\w$]+):)?([^\x08]+))\x08(,)?([^\x08]+)/gi,
rTestElseIf = /^if\s/,
rFirstElem = /<(\w+)[>\s]/,
rAttrEncode = /[\x00`><"'&]/g, // Includes > encoding since rConvertMarkers in JsViews does not skip > characters in attribute strings
rIsHtml = /[\x00`><\"'&]/,
rHasHandlers = /^on[A-Z]|^convert(Back)?$/,
rHtmlEncode = rAttrEncode,
autoTmplName = 0,
viewId = 0,
charEntities = {
"&": "&",
"<": "<",
">": ">",
"\x00": "�",
"'": "'",
'"': """,
"`": "`"
},
htmlStr = "html",
tmplAttr = "data-jsv-tmpl",
$render = {},
jsvStores = {
template: {
compile: compileTmpl
},
tag: {
compile: compileTag
},
helper: {},
converter: {}
},
// jsviews object ($.views if jQuery is loaded)
$views = {
jsviews: versionNumber,
settings: function(settings) {
$extend($viewsSettings, settings);
dbgMode($viewsSettings._dbgMode);
if ($viewsSettings.jsv) {
$viewsSettings.jsv();
}
},
sub: {
// subscription, e.g. JsViews integration
View: View,
Err: JsViewsError,
tmplFn: tmplFn,
cvt: convertArgs,
parse: parseParams,
extend: $extend,
syntaxErr: syntaxError,
onStore: {},
_lnk: retVal,
_ths: tagHandlersFromProps
},
map: dataMap, // If jsObservable loaded first, use that definition of dataMap
_cnvt: convertVal,
_tag: renderTag,
_err: error
};
function tagHandlersFromProps(tag, tagCtx) {
for (var prop in tagCtx.props) {
if (rHasHandlers.test(prop)) {
tag[prop] = tagCtx.props[prop]; // Copy over the onFoo props, convert and convertBack from tagCtx.props to tag (overrides values in tagDef).
// Note: unsupported scenario: if handlers are dynamically added ^onFoo=expression this will work, but dynamically removing will not work.
}
}
}
function retVal(val) {
return val;
}
function dbgBreak(val) {
debugger; // Insert breakpoint for debugging JsRender or JsViews.
// Consider https://github.com/BorisMoore/jsrender/issues/239: eval("debugger; //dbg"); // Insert breakpoint for debugging JsRender or JsViews. Using eval to prevent issue with minifiers (YUI Compressor)
return val;
}
function dbgMode(debugMode) {
$viewsSettings._dbgMode = debugMode;
indexStr = debugMode ? "Unavailable (nested view): use #getIndex()" : ""; // If in debug mode set #index to a warning when in nested contexts
$tags("dbg", $helpers.dbg = $converters.dbg = debugMode ? dbgBreak : retVal); // Register {{dbg/}}, {{dbg:...}} and ~dbg() to insert break points for debugging - if in debug mode.
}
function JsViewsError(message) {
// Error exception type for JsViews/JsRender
// Override of $.views.sub.Error is possible
this.name = ($.link ? "JsViews" : "JsRender") + " Error";
this.message = message || this.name;
}
function $extend(target, source) {
var name;
for (name in source) {
target[name] = source[name];
}
return target;
}
function $isFunction(ob) {
return typeof ob === "function";
}
(JsViewsError.prototype = new Error()).constructor = JsViewsError;
//========================== Top-level functions ==========================
//===================
// jsviews.delimiters
//===================
function $viewsDelimiters(openChars, closeChars, link) {
// Set the tag opening and closing delimiters and 'link' character. Default is "{{", "}}" and "^"
// openChars, closeChars: opening and closing strings, each with two characters
if (!$sub.rTag || openChars) {
delimOpenChar0 = openChars ? openChars.charAt(0) : delimOpenChar0; // Escape the characters - since they could be regex special characters
delimOpenChar1 = openChars ? openChars.charAt(1) : delimOpenChar1;
delimCloseChar0 = closeChars ? closeChars.charAt(0) : delimCloseChar0;
delimCloseChar1 = closeChars ? closeChars.charAt(1) : delimCloseChar1;
linkChar = link || linkChar;
openChars = "\\" + delimOpenChar0 + "(\\" + linkChar + ")?\\" + delimOpenChar1; // Default is "{^{"
closeChars = "\\" + delimCloseChar0 + "\\" + delimCloseChar1; // Default is "}}"
// Build regex with new delimiters
// tag (followed by / space or }) or cvtr+colon or html or code
rTag = "(?:(?:(\\w+(?=[\\/\\s\\" + delimCloseChar0 + "]))|(?:(\\w+)?(:)|(>)|!--((?:[^-]|-(?!-))*)--|(\\*)))"
+ "\\s*((?:[^\\" + delimCloseChar0 + "]|\\" + delimCloseChar0 + "(?!\\" + delimCloseChar1 + "))*?)";
// make rTag available to JsViews (or other components) for parsing binding expressions
$sub.rTag = rTag + ")";
rTag = new RegExp(openChars + rTag + "(\\/)?|(?:\\/(\\w+)))" + closeChars, "g");
// Default: bind tag converter colon html comment code params slash closeBlock
// /{(\^)?{(?:(?:(\w+(?=[\/\s}]))|(?:(\w+)?(:)|(>)|!--((?:[^-]|-(?!-))*)--|(\*)))\s*((?:[^}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}}/g
rTmplString = new RegExp("<.*>|([^\\\\]|^)[{}]|" + openChars + ".*" + closeChars);
// rTmplString looks for html tags or { or } char not preceded by \\, or JsRender tags {{xxx}}. Each of these strings are considered
// NOT to be jQuery selectors
}
return [delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1, linkChar];
}
//=========
// View.get
//=========
function getView(inner, type) { //view.get(inner, type)
if (!type) {
// view.get(type)
type = inner;
inner = undefined;
}
var views, i, l, found,
view = this,
root = !type || type === "root";
// If type is undefined, returns root view (view under top view).
if (inner) {
// Go through views - this one, and all nested ones, depth-first - and return first one with given type.
found = view.type === type ? view : undefined;
if (!found) {
views = view.views;
if (view._.useKey) {
for (i in views) {
if (found = views[i].get(inner, type)) {
break;
}
}
} else {
for (i = 0, l = views.length; !found && i < l; i++) {
found = views[i].get(inner, type);
}
}
}
} else if (root) {
// Find root view. (view whose parent is top view)
while (view.parent.parent) {
found = view = view.parent;
}
} else {
while (view && !found) {
// Go through views - this one, and all parent ones - and return first one with given type.
found = view.type === type ? view : undefined;
view = view.parent;
}
}
return found;
}
function getNestedIndex() {
var view = this.get("item");
return view ? view.index : undefined;
}
getNestedIndex.depends = function() {
return [this.get("item"), "index"];
};
function getIndex() {
return this.index;
}
getIndex.depends = function() {
return ["index"];
};
//==========
// View.hlp
//==========
function getHelper(helper) {
// Helper method called as view.hlp(key) from compiled template, for helper functions or template parameters ~foo
var wrapped,
view = this,
ctx = view.linkCtx,
res = (view.ctx || {})[helper];
if (res === undefined && ctx && ctx.ctx) {
res = ctx.ctx[helper];
}
if (res === undefined) {
res = $helpers[helper];
}
if (res) {
if ($isFunction(res) && !res._wrp) {
wrapped = function() {
// If it is of type function, and not already wrapped, we will wrap it, so if called with no this pointer it will be called with the
// view as 'this' context. If the helper ~foo() was in a data-link expression, the view will have a 'temporary' linkCtx property too.
// Note that helper functions on deeper paths will have specific this pointers, from the preceding path.
// For example, ~util.foo() will have the ~util object as 'this' pointer
return res.apply((!this || this === global) ? view : this, arguments);
};
wrapped._wrp = true;
$extend(wrapped, res); // Attach same expandos (if any) to the wrapped function
}
}
return wrapped || res;
}
//==============
// jsviews._cnvt
//==============
function convertVal(converter, view, tagCtx, onError) {
// self is template object or linkCtx object
var tag, value,
// if tagCtx is an integer, then it is the key for the compiled function to return the boundTag tagCtx
boundTag = +tagCtx === tagCtx && view.tmpl.bnds[tagCtx-1],
linkCtx = view.linkCtx; // For data-link="{cvt:...}"...
onError = onError !== undefined && {props: {}, args: [onError]};
tagCtx = onError || (boundTag ? boundTag(view.data, view, $views) : tagCtx);
value = tagCtx.args[0];
if (converter || boundTag) {
tag = linkCtx && linkCtx.tag;
if (!tag) {
tag = {
_: {
inline: !linkCtx,
bnd: boundTag
},
tagName: ":",
cvt: converter,
flow: true,
tagCtx: tagCtx,
_is: "tag"
};
if (linkCtx) {
linkCtx.tag = tag;
tag.linkCtx = linkCtx;
tagCtx.ctx = extendCtx(tagCtx.ctx, linkCtx.view.ctx);
}
$sub._lnk(tag);
}
tag._er = onError && value;
tagHandlersFromProps(tag, tagCtx);
tagCtx.view = view;
tag.ctx = tagCtx.ctx || {};
delete tagCtx.ctx;
// Provide this tag on view, for addBindingMarkers on bound tags to add the tag to view._.bnds, associated with the tag id,
view._.tag = tag;
value = convertArgs(tag, tag.convert || converter !== "true" && converter)[0]; // If there is a convertBack but no convert, converter will be "true"
// Call onRender (used by JsViews if present, to add binding annotations around rendered content)
value = boundTag && view._.onRender
? view._.onRender(value, view, boundTag)
: value;
view._.tag = undefined;
}
return value != undefined ? value : "";
}
function convertArgs(tag, converter) {
var tagCtx = tag.tagCtx,
view = tagCtx.view,
args = tagCtx.args;
converter = converter && ("" + converter === converter
? (view.getRsc("converters", converter) || error("Unknown converter: '" + converter + "'"))
: converter);
args = !args.length && !tagCtx.index // On the opening tag with no args, bind to the current data context
? [view.data]
: converter
? args.slice() // If there is a converter, use a copy of the tagCtx.args array for rendering, and replace the args[0] in
// the copied array with the converted value. But we do not modify the value of tag.tagCtx.args[0] (the original args array)
: args; // If no converter, render with the original tagCtx.args
if (converter) {
if (converter.depends) {
tag.depends = $sub.getDeps(tag.depends, tag, converter.depends, converter);
}
args[0] = converter.apply(tag, args);
}
return args;
}
//=============
// jsviews._tag
//=============
function getResource(resourceType, itemName) {
var res, store,
view = this;
while ((res === undefined) && view) {
store = view.tmpl[resourceType];
res = store && store[itemName];
view = view.parent;
}
return res || $views[resourceType][itemName];
}
function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) {
// Called from within compiled template function, to render a template tag
// Returns the rendered tag
var tag, tags, attr, parentTag, i, l, itemRet, tagCtx, tagCtxCtx, content, tagDef,
callInit, mapDef, thisMap, args, props, initialTmpl,
ret = "",
linkCtx = parentView.linkCtx || 0,
ctx = parentView.ctx,
parentTmpl = tmpl || parentView.tmpl,
// if tagCtx is an integer, then it is the key for the compiled function to return the boundTag tagCtxs
boundTag = +tagCtxs === tagCtxs && parentTmpl.bnds[tagCtxs-1];
if (tagName._is === "tag") {
tag = tagName;
tagName = tag.tagName;
tagCtxs = tag.tagCtxs;
}
tag = tag || linkCtx.tag;
onError = onError !== undefined && (ret += onError, [{props: {}, args: []}]);
tagCtxs = onError || (boundTag ? boundTag(parentView.data, parentView, $views) : tagCtxs);
l = tagCtxs.length;
for (i = 0; i < l; i++) {
if (!i && (!tmpl || !tag)) {
tagDef = parentView.getRsc("tags", tagName) || error("Unknown tag: {{" + tagName + "}}");
}
tagCtx = tagCtxs[i];
if (!linkCtx.tag || tag._er) {
// We are initializing tag, so for block tags, tagCtx.tmpl is an integer > 0
content = tagCtx.tmpl;
content = tagCtx.content = content && parentTmpl.tmpls[content - 1];
$extend(tagCtx, {
tmpl: (tag ? tag : tagDef).template || content, // Set the tmpl property to the content of the block tag
render: renderContent,
index: i,
view: parentView,
ctx: extendCtx(tagCtx.ctx, ctx) // Extend parentView.ctx
// Possible future feature:
//var updatedValueOfArg0 = this.tagCtx.get(0);
//var updatedValueOfPropFoo = this.tagCtx.get("foo");
//var updatedValueOfCtxPropFoo = this.tagCtx.get("~foo");
//_fns: {},
//get: function(key) {
// return (this._fns[key] = this._fns[key] || new Function("data,view,j,u",
// "return " + $.views.sub.parse(this.params[+key === key ? "args" : (key.charAt(0) === "~" ? (key = key.slice(1), "ctx") : "props")][key]) + ";")
// )(this.view.data, this.view, $views);
//},
});
}
if (tmpl = tagCtx.props.tmpl) {
// If the tmpl property is overridden, set the value (when initializing, or, in case of binding: ^tmpl=..., when updating)
tmpl = "" + tmpl === tmpl // if a string
? parentView.getRsc("templates", tmpl) || $templates(tmpl)
: tmpl;
tagCtx.tmpl = tmpl;
}
if (!tag) {
// This will only be hit for initial tagCtx (not for {{else}}) - if the tag instance does not exist yet
// Instantiate tag if it does not yet exist
if (tagDef._ctr) {
// If the tag has not already been instantiated, we will create a new instance.
// ~tag will access the tag, even within the rendering of the template content of this tag.
// From child/descendant tags, can access using ~tag.parent, or ~parentTags.tagName
tag = new tagDef._ctr();
callInit = !!tag.init;
} else {
// This is a simple tag declared as a function, or with init set to false. We won't instantiate a specific tag constructor - just a standard instance object.
$sub._lnk(tag = {
// tag instance object if no init constructor
render: tagDef.render
});
}
tag._ = {
inline: !linkCtx
};
if (linkCtx) {
linkCtx.tag = tag;
tag.linkCtx = linkCtx;
}
if (tag._.bnd = boundTag || linkCtx.fn) {
// Bound if {^{tag...}} or data-link="{tag...}"
tag._.arrVws = {};
} else if (tag.dataBoundOnly) {
error("{^{" + tagName + "}} tag must be data-bound");
}
tag.tagName = tagName;
tag.parent = parentTag = ctx && ctx.tag;
tag._is = "tag";
tag._def = tagDef;
tag.tagCtxs = tagCtxs;
//TODO better perf for childTags() - keep child tag.tags array, (and remove child, when disposed)
// tag.tags = [];
// Provide this tag on view, for addBindingMarkers on bound tags to add the tag to view._.bnds, associated with the tag id
}
tagCtx.tag = tag;
if (tag.dataMap && tag.tagCtxs) {
tagCtx.map = tag.tagCtxs[i].map; // Copy over the compiled map instance from the previous tagCtxs to the refreshed ones
}
if (!tag.flow) {
tagCtxCtx = tagCtx.ctx = tagCtx.ctx || {};
// tags hash: tag.ctx.tags, merged with parentView.ctx.tags,
tags = tag.parents = tagCtxCtx.parentTags = ctx && extendCtx(tagCtxCtx.parentTags, ctx.parentTags) || {};
if (parentTag) {
tags[parentTag.tagName] = parentTag;
//TODO better perf for childTags: parentTag.tags.push(tag);
}
tags[tag.tagName] = tagCtxCtx.tag = tag;
}
}
parentView._.tag = tag;
if (!(tag._er = onError)) {
tagHandlersFromProps(tag, tagCtxs[0]);
tag.rendering = {}; // Provide object for state during render calls to tag and elses. (Used by {{if}} and {{for}}...)
for (i = 0; i < l; i++) {
tagCtx = tag.tagCtx = tag.tagCtxs[i];
props = tagCtx.props;
args = convertArgs(tag, tag.convert);
if (mapDef = props.dataMap || tag.dataMap) {
if (args.length || props.dataMap) {
thisMap = tagCtx.map;
if (!thisMap || thisMap.src !== args[0] || isUpdate) {
if (thisMap && thisMap.src) {
thisMap.unmap(); // only called if observable map - not when only used in JsRender, e.g. by {{props}}
}
thisMap = tagCtx.map = mapDef.map(args[0], props);
}
args = [thisMap.tgt];
}
}
tag.ctx = tagCtx.ctx;
if (!i && callInit) {
initialTmpl = tag.template;
tag.init(tagCtx, linkCtx, tag.ctx);
callInit = undefined;
if (tag.template !== initialTmpl) {
tag._.tmpl = tag.template; // This will override the tag.template and also tagCtx.props.tmpl for all tagCtxs
}
if (linkCtx) {
// Set attr on linkCtx to ensure outputting to the correct target attribute.
// Setting either linkCtx.attr or this.attr in the init() allows per-instance choice of target attrib.
linkCtx.attr = tag.attr = linkCtx.attr || tag.attr;
}
}
itemRet = undefined;
if (tag.render) {
itemRet = tag.render.apply(tag, args);
}
args = args.length ? args : [parentView]; // no arguments - get data context from view.
itemRet = itemRet !== undefined
? itemRet // Return result of render function unless it is undefined, in which case return rendered template
: tagCtx.render(args[0], true) || (isUpdate ? undefined : "");
// No return value from render, and no template/content tagCtx.render(...), so return undefined
ret = ret ? ret + (itemRet || "") : itemRet; // If no rendered content, this will be undefined
}
delete tag.rendering;
}
tag.tagCtx = tag.tagCtxs[0];
tag.ctx = tag.tagCtx.ctx;
if (tag._.inline && (attr = tag.attr) && attr !== htmlStr) {
// inline tag with attr set to "text" will insert HTML-encoded content - as if it was element-based innerText
ret = attr === "text"
? $converters.html(ret)
: "";
}
return boundTag && parentView._.onRender
// Call onRender (used by JsViews if present, to add binding annotations around rendered content)
? parentView._.onRender(ret, parentView, boundTag)
: ret;
}
//=================
// View constructor
//=================
function View(context, type, parentView, data, template, key, contentTmpl, onRender) {
// Constructor for view object in view hierarchy. (Augmented by JsViews if JsViews is loaded)
var views, parentView_, tag,
self = this,
isArray = type === "array",
self_ = {
key: 0,
useKey: isArray ? 0 : 1,
id: "" + viewId++,
onRender: onRender,
bnds: {}
};
self.data = data;
self.tmpl = template,
self.content = contentTmpl;
self.views = isArray ? [] : {};
self.parent = parentView;
self.type = type || "top";
// If the data is an array, this is an 'array view' with a views array for each child 'item view'
// If the data is not an array, this is an 'item view' with a views 'hash' object for any child nested views
// ._.useKey is non zero if is not an 'array view' (owning a data array). Use this as next key for adding to child views hash
self._ = self_;
self.linked = !!onRender;
if (parentView) {
views = parentView.views;
parentView_ = parentView._;
if (parentView_.useKey) {
// Parent is an 'item view'. Add this view to its views object
// self._key = is the key in the parent view hash
views[self_.key = "_" + parentView_.useKey++] = self;
self.index = indexStr;
self.getIndex = getNestedIndex;
tag = parentView_.tag;
self_.bnd = isArray && (!tag || !!tag._.bnd && tag); // For array views that are data bound for collection change events, set the
// view._.bnd property to true for top-level link() or data-link="{for}", or to the tag instance for a data-bound tag, e.g. {^{for ...}}
} else {
// Parent is an 'array view'. Add this view to its views array
views.splice(
// self._.key = self.index - the index in the parent view array
self_.key = self.index = key,
0, self);
}
// If no context was passed in, use parent context
// If context was passed in, it should have been merged already with parent context
self.ctx = context || parentView.ctx;
} else {
self.ctx = context;
}
}
View.prototype = {
get: getView,
getIndex: getIndex,
getRsc: getResource,
hlp: getHelper,
_is: "view"
};
//=============
// Registration
//=============
function compileChildResources(parentTmpl) {
var storeName, resources, resourceName, resource, settings, compile, onStore;
for (storeName in jsvStores) {
settings = jsvStores[storeName];
if ((compile = settings.compile) && (resources = parentTmpl[storeName + "s"])) {
for (resourceName in resources) {
// compile child resource declarations (templates, tags, tags["for"] or helpers)
resource = resources[resourceName] = compile(resourceName, resources[resourceName], parentTmpl);
if (resource && (onStore = $sub.onStore[storeName])) {
// e.g. JsViews integration
onStore(resourceName, resource, compile);
}
}
}
}
}
function compileTag(name, tagDef, parentTmpl) {
var init, tmpl;
if ($isFunction(tagDef)) {
// Simple tag declared as function. No presenter instantation.
tagDef = {
depends: tagDef.depends,
render: tagDef
};
} els