From c3a5a416de2d0f44aeabb4b093ca4d5f10c7fd41 Mon Sep 17 00:00:00 2001 From: Antonio Stoilkov Date: Thu, 14 May 2015 13:33:08 +0300 Subject: [PATCH] jsblocks TodoMVC implementation --- examples/jsblocks/.gitignore | 9 + examples/jsblocks/index.html | 59 + examples/jsblocks/js/app.js | 152 + .../jsblocks/node_modules/blocks/blocks.js | 14724 ++++++++++++++++ .../node_modules/todomvc-app-css/index.css | 378 + .../node_modules/todomvc-common/base.css | 141 + .../node_modules/todomvc-common/base.js | 249 + examples/jsblocks/package.json | 8 + examples/jsblocks/readme.md | 29 + index.html | 3 + learn.json | 37 + 11 files changed, 15789 insertions(+) create mode 100644 examples/jsblocks/.gitignore create mode 100644 examples/jsblocks/index.html create mode 100644 examples/jsblocks/js/app.js create mode 100644 examples/jsblocks/node_modules/blocks/blocks.js create mode 100644 examples/jsblocks/node_modules/todomvc-app-css/index.css create mode 100644 examples/jsblocks/node_modules/todomvc-common/base.css create mode 100644 examples/jsblocks/node_modules/todomvc-common/base.js create mode 100644 examples/jsblocks/package.json create mode 100644 examples/jsblocks/readme.md diff --git a/examples/jsblocks/.gitignore b/examples/jsblocks/.gitignore new file mode 100644 index 0000000000..49268030e6 --- /dev/null +++ b/examples/jsblocks/.gitignore @@ -0,0 +1,9 @@ +node_modules/blocks/* +!node_modules/blocks/blocks.js + +node_modules/todomvc-app-css/* +!node_modules/todomvc-app-css/index.css + +node_modules/todomvc-common/* +!node_modules/todomvc-common/base.css +!node_modules/todomvc-common/base.js diff --git a/examples/jsblocks/index.html b/examples/jsblocks/index.html new file mode 100644 index 0000000000..cfb58f2d3f --- /dev/null +++ b/examples/jsblocks/index.html @@ -0,0 +1,59 @@ + + + + + + jsblocks • TodoMVC + + + + +
+
+

todos

+ +
+ +
+ + {{todos.remaining}} {{todos.remaining() == 1 ? 'item' : 'items'}} left + + + +
+
+ + + + + + diff --git a/examples/jsblocks/js/app.js b/examples/jsblocks/js/app.js new file mode 100644 index 0000000000..712796d693 --- /dev/null +++ b/examples/jsblocks/js/app.js @@ -0,0 +1,152 @@ +/*global blocks */ + +(function () { + 'use strict'; + + var ENTER_KEY = 13; + var ESCAPE_KEY = 27; + + var App = blocks.Application(); + + var Todo = App.Model({ + title: App.Property(), + + completed: App.Property(), + + editing: blocks.observable(), + + init: function () { + var collection = this.collection(); + + // collection is undefined when a Todo is still not part of the Todos collection + if (collection) { + // save to Local Storage on each attribute change + this.title.on('change', collection.save); + this.completed.on('change', collection.save); + } + + this.title.on('change', function (newValue) { + this.title((newValue || '').trim()); + }); + }, + + toggleComplete: function () { + this.completed(!this.completed()); + }, + + edit: function () { + this.lastValue = this.title(); + this.editing(true); + }, + + closeEdit: function () { + if (this.title()) { + this.editing(false); + } else { + this.destroy(); + } + }, + + handleAction: function (e) { + if (e.which === ENTER_KEY) { + this.closeEdit(); + } else if (e.which === ESCAPE_KEY) { + this.title(this.lastValue); + this.editing(false); + } + } + }); + + var Todos = App.Collection(Todo, { + remaining: blocks.observable(), + + init: function () { + this + // load the data from the Local Storage + .reset(JSON.parse(localStorage.getItem('todos-jsblocks')) || []) + // save to Local Storage on each item add or remove + .on('add remove', this.save) + .updateRemaining(); + }, + + // set all todos as completed + toggleAll: function () { + var complete = this.remaining() === 0 ? false : true; + this.each(function (todo) { + todo.completed(complete); + }); + }, + + // remove all completed todos + clearCompleted: function () { + this.removeAll(function (todo) { + return todo.completed(); + }); + }, + + // saves all data back to the Local Storage + save: function () { + var result = []; + + blocks.each(this(), function (model) { + result.push(model.dataItem()); + }); + + localStorage.setItem('todos-jsblocks', JSON.stringify(result)); + + this.updateRemaining(); + }, + + // updates the observable + updateRemaining: function () { + this.remaining(this.reduce(function (memo, todo) { + return todo.completed() ? memo : memo + 1; + }, 0)); + } + }); + + App.View('Todos', { + options: { + // creates a route for the View in order to handle + // /all, /active, /completed filters + route: blocks.route('{{filter}}').optional('filter') + }, + + filter: blocks.observable(), + + newTodo: new Todo(), + + // holds all todos for the current view + // todos are filtered if "Active" or "Completed" is clicked + todos: new Todos().extend('filter', function (value) { + var mode = this.filter(); + var completed = value.completed(); + var include = true; + + if (mode === 'active') { + include = !completed; + } else if (mode === 'completed') { + include = completed; + } + + return include; + }), + + // filter the data when the route have changed + // the callback is fired when "All", "Active" or "Completed" have been clicked + routed: function (params) { + if (params.filter !== 'active' && params.filter !== 'completed') { + params.filter = 'all'; + } + this.filter(params.filter); + }, + + addTodo: function (e) { + if (e.which === ENTER_KEY && this.newTodo.title()) { + this.todos.push(this.newTodo); + // return all Todo values to their defaults + this.newTodo.reset(); + } + } + }); +})(); diff --git a/examples/jsblocks/node_modules/blocks/blocks.js b/examples/jsblocks/node_modules/blocks/blocks.js new file mode 100644 index 0000000000..78f6f2ced5 --- /dev/null +++ b/examples/jsblocks/node_modules/blocks/blocks.js @@ -0,0 +1,14724 @@ +/*! + * jsblocks JavaScript Library v@VERSION + * http://jsblocks.com/ + * + * Copyright 2014, Antonio Stoilkov + * Released under the MIT license + * http://jsblocks.org/license + * + * Date: @DATE + */ +(function(global, factory) { + if (typeof module === 'object' && typeof module.exports === 'object') { + module.exports = factory(global, true); + } else { + factory(global); + } + + // Pass this if window is not defined yet +}(typeof window !== 'undefined' ? window : this, function(global, noGlobal) { + var toString = Object.prototype.toString; + var slice = Array.prototype.slice; + var hasOwn = Object.prototype.hasOwnProperty; + var support = {}; + var core = {}; + + /** + * @namespace blocks + */ + var blocks = function (value) { + if (core.expressionsCreated) { + if (arguments.length === 0) { + return core.staticExpression; + } + return core.createExpression(value); + //return core.createExpression(blocks.unwrap(value)); + } + return value; + }; + + blocks.version = '0.3.4'; + blocks.core = core; + + /** + * Copies properties from all provided objects into the first object parameter + * + * @memberof blocks + * @param {Object} obj + * @param {...Object} objects + * @returns {Object} + */ + blocks.extend = function() { + var src, copyIsArray, copy, name, options, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if (typeof target !== 'object' && !blocks.isFunction(target)) { + target = {}; + } + + for (; i < length; i++) { + // Only deal with non-null/undefined values + if ((options = arguments[i]) != null) { + // Extend the base object + for (name in options) { + src = target[name]; + copy = options[name]; + + // Prevent never-ending loop + if (target === copy) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if (deep && copy && (blocks.isPlainObject(copy) || (copyIsArray = blocks.isArray(copy)))) { + if (copyIsArray) { + copyIsArray = false; + clone = src && blocks.isArray(src) ? src : []; + } else { + clone = src && blocks.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[name] = blocks.extend(deep, clone, copy); + + } else { + target[name] = copy; + } + } + } + } + + // Return the modified object + return target; + }; + + /** + * @callback iterateCallback + * @param {*} value - The value + * @param {(number|string)} indexOrKey - Index or key + * @param {(Array|Object)} collection - The collection that is being iterated + */ + + /** + * Iterates over the collection + * + * @memberof blocks + * @param {(Array|Object)} collection - The array or object to iterate over + * @param {Function} callback - The callback that will be executed for each element in the collection + * @param {*} [thisArg] - Optional this context for the callback + * + * @example {javascript} + * blocks.each([3, 1, 4], function (value, index, collection) { + * // value is the current item (3, 1 and 4) + * // index is the current index (0, 1 and 2) + * // collection points to the array passed to the function - [3, 1, 4] + * }); + */ + blocks.each = function(collection, callback, thisArg) { + if (collection == null) { + return; + } + + var length = collection.length; + var indexOrKey = -1; + var isArray = typeof length == 'number'; + + callback = parseCallback(callback, thisArg); + + if (isArray) { + while (++indexOrKey < length) { + if (callback(collection[indexOrKey], indexOrKey, collection) === false) { + break; + } + } + } else { + for (indexOrKey in collection) { + if (callback(collection[indexOrKey], indexOrKey, collection) === false) { + break; + } + } + } + }; + + /** + * Iterates over the collection from end to start + * + * @memberof blocks + * @param {(Array|Object)} collection - The array or object to iterate over + * @param {Function} callback - The callback that will be executed for each element in the collection + * @param {*} [thisArg] - Optional this context for the callback + * + * @example {javascript} + * blocks.eachRight([3, 1, 4], function (value, index, collection) { + * // value is the current item (4, 1 and 3) + * // index is the current index (2, 1 and 0) + * // collection points to the array passed to the function - [3, 1, 4] + * }); + */ + blocks.eachRight = function(collection, callback, thisArg) { + if (collection == null) { + return; + } + + var length = collection.length, + indexOrKey = collection.length, + isCollectionAnArray = typeof length == 'number'; + + callback = parseCallback(callback, thisArg); + + if (isCollectionAnArray) { + while (--indexOrKey >= 0) { + callback(collection[indexOrKey], indexOrKey, collection); + } + } else { + for (indexOrKey in collection) { + callback(collection[indexOrKey], indexOrKey, collection); + } + } + }; + + blocks.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(type) { + blocks['is' + type] = function(obj) { + return toString.call(obj) == '[object ' + type + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable 'Arguments' type. + if (!blocks.isArguments(arguments)) { + blocks.isArguments = function(obj) { + return !!(obj && hasOwn.call(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof(/./) !== 'function') { + blocks.isFunction = function(obj) { + return !!(obj && typeof obj === 'function'); + }; + } + + /** + * Determines if a value is an array. + * Returns false for array like objects (for example arguments object) + * + * @memberof blocks + * @param {*} value - The value to check if it is an array + * @returns {boolean} - Whether the value is an array + * + * @example {javascript} + * blocks.isArray([1, 2, 3]); + * // -> true + * + * function calculate() { + * blocks.isArray(arguments); + * // -> false + * } + */ + blocks.isArray = Array.isArray || function(value) { + return toString.call(value) == '[object Array]'; + }; + + blocks.extend(blocks, { + + /** + * Represents a dummy empty function + * + * @memberof blocks + * @returns {Function} - Empty function + * + * @example {javascript} + * function max(collection, callback) { + * callback = callback || blocks.noop; + * } + */ + noop: function() {}, + + inherit: function(BaseClass, Class, prototype) { + if ((arguments.length < 3 && blocks.isPlainObject(Class)) || arguments.length == 1) { + prototype = Class; + Class = BaseClass; + BaseClass = undefined; + } + + if (BaseClass) { + Class.prototype = objectCreate(BaseClass.prototype); + Class.prototype.constructor = Class; + blocks.extend(Class.prototype, prototype); + Class.prototype.__Class__ = BaseClass; + Class.prototype._super = _super; + } else if (prototype) { + Class.prototype = prototype; + } + + return Class; + }, + + /** + * Determines the true type of an object + * + * @memberof blocks + * @param {*} value - The value for which to determine its type + * @returns {string} - Returns the type of the value as a string + * + * @example {javascript} + * blocks.type('a string'); + * // -> string + * + * blocks.type(314); + * // -> number + * + * blocks.type([]); + * // -> array + * + * blocks.type({}); + * // -> object + * + * blocks.type(blocks.noop); + * // -> function + * + * blocks.type(new RegExp()); + * // -> regexp + * + * blocks.type(undefined); + * // -> undefined + * + * blocks.type(null); + * // -> null + */ + type: function(value) { + if (value instanceof Array) { + return 'array'; + } + if (typeof value == 'string' || value instanceof String) { + return 'string'; + } + if (typeof value == 'number' || value instanceof Number) { + return 'number'; + } + if (value instanceof Date) { + return 'date'; + } + if (toString.call(value) === '[object RegExp]') { + return 'regexp'; + } + if (value === null) { + return 'null'; + } + if (value === undefined) { + return 'undefined'; + } + + if (blocks.isFunction(value)) { + return 'function'; + } + + if (blocks.isBoolean(value)) { + return 'boolean'; + } + + return 'object'; + }, + + /** + * Determines if a specific value is the specified type + * + * @memberof blocks + * @param {*} value - The value + * @param {string} type - The type + * @returns {boolean} - If the value is from the specified type + * + * @example {javascript} + * blocks.is([], 'array'); + * // -> true + * + * blocks.is(function () {}, 'object'); + * // -> false + */ + is: function(value, type) { + if (arguments.length > 1 && blocks.isFunction(type)) { + return type.prototype.isPrototypeOf(value); + } + return blocks.type(value) == type; + }, + + /** + * Checks if a variable has the specified property. + * Uses hasOwnProperty internally + * + * @memberof blocks + * @param {*} obj - The object to call hasOwnPrototype for + * @param {String} key - The key to check if exists in the object + * @returns {boolean} Returns if the key exists in the provided object + * + * @example {javascript} + * blocks.has({ + * price: undefined + * }, 'price'); + * // -> true + * + * blocks.has({ + * price: 314 + * }, 'ratio'); + * // -> false + */ + has: function(obj, key) { + return !!(obj && hasOwn.call(obj, key)); + }, + + hasValue: function(value) { + return value != null && (!blocks.isNumber(value) || !isNaN(value)); + }, + + toString: function(value) { + // TODO: Implement and make tests + var result = ''; + if (blocks.hasValue(value)) { + result = value.toString(); + } + return result; + }, + + /** + * Unwraps a jsblocks value to its raw representation. + * Unwraps blocks.observable() and blocks() values + * + * @memberof blocks + * @param {*} value - The value that will be unwrapped + * @returns {*} The unwrapped value + * + * @example {javascript} + * blocks.unwrap(blocks.observable(314)); + * // -> 314 + * + * blocks.unwrap(blocks([3, 1, 4])); + * // -> [3, 1, 4] + * + * blocks.unwrap('a string or any other value will not be changed'); + * // -> 'a string or any other value will not be changed' + */ + unwrap: function(value) { + if (core.expressionsCreated && core.isExpression(value)) { + return value.value(); + } + + if (blocks.unwrapObservable) { + return blocks.unwrapObservable(value); + } + return value; + }, + + /** + * Unwraps a jQuery instance and returns the first element + * + * @param {*} element - If jQuery element is specified it will be unwraped + * @returns {*} - The unwraped value + * + * @example {javascript} + * var articles = $('.article'); + * blocks.$unwrap() + */ + $unwrap: function(element, callback, thisArg) { + callback = parseCallback(callback, thisArg); + + if (element && element.jquery) { + if (callback) { + element.each(function () { + callback(this); + }); + } + element = element[0]; + } else { + if (callback) { + callback(element); + } + } + + return element; + }, + + /** + * Converts a value to an array. Arguments object is converted to array + * and primitive values are wrapped in an array. Does nothing when value + * is already an array + * + * @memberof blocks + * @param {*} value - The value to be converted to an array + * @returns {Array} - The array + * + * @example {javascript} + * blocks.toArray(3); + * // -> [3] + * + * function calculate() { + * var numbers = blocks.toArray(arguments); + * } + * + * blocks.toArray([3, 1, 4]); + * // -> [3, 1, 4] + */ + toArray: function(value) { + // TODO: Think if it should be removed permanantely. + // Run tests after change to observe difference + //if (value == null) { + // return []; + //} + if (blocks.isArguments(value)) { + return slice.call(value); + } + if (blocks.isElements(value)) { + // TODO: if not IE8 and below use slice.call + /* jshint newcap: false */ + var result = Array(value.length); + var index = -1; + var length = value.length; + while (++index < length) { + result[index] = value[index]; + } + return result; + } + if (!blocks.isArray(value)) { + return [value]; + } + return value; + }, + + /** + * Converts an integer or string to a unit. + * If the value could not be parsed to a number it is not converted + * + * @memberof blocks + * @param {[type]} value - The value to be converted to the specified unit + * @param {String} [unit='px'] - Optionally provide a unit to convert to. + * Default value is 'px' + * + * @example {javascript} + * + * blocks.toUnit(230); + * // -> 230px + * + * blocks.toUnit(230, '%'); + * // -> 230% + * + * blocks.toUnit('60px', '%'); + * // -> 60% + */ + toUnit: function(value, unit) { + var unitIsSpecified = unit; + unit = unit || 'px'; + + if (blocks.isNaN(parseFloat(value))) { + return value; + } + + if (blocks.isString(value) && blocks.isNaN(parseInt(value.charAt(value.length - 1), 10))) { + if (unitIsSpecified) { + return value.replace(/[^0-9]+$/, unit); + } + return value; + } + return value + unit; + }, + + /** + * Clones value. If deepClone is set to true the value will be cloned recursively + * + * @memberof blocks + * @param {*} value - + * @param {boolean} [deepClone] - Description + * @returns {*} Description + * + * @example {javascript} + * var array = [3, 1, 4]; + * var cloned = blocks.clone(array); + * // -> [3, 1, 4] + * var areEqual = array == cloned; + * // -> false + */ + clone: function(value, deepClone) { + if (value == null) { + return value; + } + + var type = blocks.type(value); + var clone; + var key; + + if (type == 'array') { + return value.slice(0); + } else if (type == 'object') { + if (value.constructor === Object) { + clone = {}; + } else { + clone = new value.constructor(); + } + + for (key in value) { + clone[key] = deepClone ? blocks.clone(value[key], true) : value[key]; + } + return clone; + } else if (type == 'date') { + return new Date(value.getFullYear(), value.getMonth(), value.getDate(), + value.getHours(), value.getMinutes(), value.getSeconds(), value.getMilliseconds()); + } else if (type == 'string') { + return value.toString(); + } else if (type == 'regexp') { + var flags = ''; + if (value.global) { + flags += 'g'; + } + if (value.ignoreCase) { + flags += 'i'; + } + if (value.multiline) { + flags += 'm'; + } + clone = new RegExp(value.source, flags); + clone.lastIndex = value.lastIndex; + return clone; + } + + return value; + }, + + /** + * Determines if the specified value is a HTML elements collection + * + * @memberof blocks + * @param {*} value - The value to check if it is elements collection + * @returns {boolean} - Returns whether the value is elements collection + */ + isElements: function(value) { + var isElements = false; + if (value) { + if (typeof HTMLCollection != 'undefined') { + isElements = value instanceof window.HTMLCollection; + } + if (typeof NodeList != 'undefined' && !isElements) { + isElements = value instanceof NodeList; + } + if (!isElements && blocks.isString(value.item)) { + try { + value.item(0); + isElements = true; + } catch (e) {} + } + } + return isElements; + }, + + /** + * Determines if the specified value is a HTML element + * + * @memberof blocks + * @param {*} value - The value to check if it is a HTML element + * @returns {boolean} - Returns whether the value is a HTML element + * + * @example {javascript} + * blocks.isElement(document.body); + * // -> true + * + * blocks.isElement({}); + * // -> false + */ + isElement: function(value) { + return !!(value && value.nodeType === 1); + }, + + /** + * Determines if a the specified value is a boolean. + * + * @memberof blocks + * @param {*} value - The value to be checked if it is a boolean + * @returns {boolean} - Whether the value is a boolean or not + * + * @example {javascript} + * blocks.isBoolean(true); + * // -> true + * + * blocks.isBoolean(new Boolean(false)); + * // -> true + * + * blocks.isBoolean(1); + * // -> false + */ + isBoolean: function(value) { + return value === true || value === false || toString.call(value) == '[object Boolean]'; + }, + + /** + * Determines if the specified value is an object + * + * @memberof blocks + * @param {[type]} obj - The value to check for if it is an object + * @returns {boolean} - Returns whether the value is an object + */ + isObject: function(obj) { + return obj === Object(obj); + }, + + /** + * Determines if a value is a object created using {} or new Object + * + * @memberof blocks + * @param {*} obj - The value that will be checked + * @returns {boolean} - Whether the value is a plain object or not + * + * @example {javascript} + * blocks.isPlainObject({ property: true }); + * // -> true + * + * blocks.isPlainObject(new Object()); + * // -> true + * + * function Car () { + * + * } + * + * blocks.isPlainObject(new Car()); + * // -> false + */ + isPlainObject: function(obj) { + var key; + + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if (!obj || typeof obj !== 'object' || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.window == obj) { + return false; + } + + try { + // Not own constructor property must be Object + if (obj.constructor && !hasOwn.call(obj, 'constructor') && !hasOwn.call(obj.constructor.prototype, 'isPrototypeOf')) { + return false; + } + } catch (e) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Support: IE<9 + // Handle iteration over inherited properties before own properties. + if (support.ownPropertiesAreLast) { + for (key in obj) { + return hasOwn.call(obj, key); + } + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + // jshint noempty: false + // Disable JSHint error: Empty blocks. This option warns when you have an empty block in your code. + for (key in obj) {} + + return key === undefined || hasOwn.call(obj, key); + }, + + isFinite: function(value) { + return isFinite(value) && !blocks.isNaN(parseFloat(value)); + }, + + isNaN: function(value) { + return blocks.isNumber(value) && value != +value; + }, + + isNull: function(value) { + return value === null; + }, + + isUndefined: function(value) { + return value === undefined; + }, + + nothing: {}, + + access: function(obj, path, defaultValue) { + var index = 0; + var name; + + defaultValue = arguments.length > 2 ? defaultValue : blocks.nothing; + path = path.split('.'); + name = path[0]; + + while (name) { + if (obj == null) { + return defaultValue; + } + obj = obj[name]; + name = path[++index]; + } + return obj; + }, + + swap: function(array, indexA, indexB) { + var length = array.length; + if (indexA >= 0 && indexB >= 0 && indexA < length && indexB < length) { + array[indexA] = array[indexB] + (array[indexB] = array[indexA], 0); + } + return array; + }, + + move: function(array, sourceIndex, targetIndex) { + if (sourceIndex != targetIndex) { + if (sourceIndex <= targetIndex) { + targetIndex++; + } + array.splice(targetIndex, 0, array[sourceIndex]); + if (sourceIndex > targetIndex) { + sourceIndex++; + } + array.splice(sourceIndex, 1); + } + return array; + }, + + /** + * Changes the this binding to a function and optionally passes additional parameters to the + * function + * + * @memberof blocks + * @param {Function} func - The function for which to change the this binding and optionally + * add arguments + * @param {*} thisArg - The new this binding context value + * @param {...*} [args] - Optional arguments that will be passed to the function + * @returns {Function} - The newly created function having the new this binding and optional + * arguments + * + * @example {javascript} + * var alert = blocks.bind(function () { + * alert(this); + * }, 'Hello bind method!'); + * + * alert(); + * // -> alerts 'Hello bind method' + * + * var alertAll = blocks.bind(function (firstName, lastName) { + * alert('My name is ' + firstName + ' ' + lastName); + * }, null, 'John', 'Doe'); + * + * alertAll(); + * // -> alerts 'My name is John Doe' + */ + bind: function(func, thisArg) { + var Class = function() {}; + var args = slice.call(arguments, 2); + var bound; + + bound = function() { + if (!(this instanceof bound)) { + return func.apply(thisArg, args.concat(slice.call(arguments))); + } + Class.prototype = func.prototype; + var self = new Class(); + //Class.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) { + return result; + } + return self; + }; + return bound; + }, + + /** + * Determines if two values are deeply equal. + * Set deepEqual to false to stop recusively equality checking + * + * @memberof blocks + * @param {*} a - The first object to be campared + * @param {*} b - The second object to be compared + * @param {boolean} [deepEqual] - Determines if the equality check will recursively check all + * child properties + * @returns {boolean} - Whether the two values are equal + * + * @example {javascript} + * blocks.equals([3, 4], [3, 4]); + * // -> true + * + * blocks.equals({ value: 7 }, { value: 7, result: 1}); + * // -> false + */ + equals: function(a, b, deepEqual) { + // TODO: deepEqual could accept a Number which represents the levels it could go in the recursion + a = blocks.unwrap(a); + b = blocks.unwrap(b); + return equals(a, b, [], [], deepEqual); + } + }); + + // Internal recursive comparison function for `isEqual`. + function equals(a, b, aStack, bStack, deepEqual) { + if (deepEqual !== false) { + deepEqual = true; + } + + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) { + return a !== 0 || 1 / a == 1 / b; + } + + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) { + return a === b; + } + + // Unwrap any wrapped objects. + if (a instanceof blocks) { + a = a._wrapped; + } + if (b instanceof blocks) { + b = b._wrapped; + } + + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) { + return false; + } + + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `'5'` is + // equivalent to `new String('5')`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + + if (typeof a != 'object' || typeof b != 'object') { + return false; + } + + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) { + return bStack[length] == b; + } + } + + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, + bCtor = b.constructor; + if (aCtor !== bCtor && !(blocks.isFunction(aCtor) && (aCtor instanceof aCtor) && + blocks.isFunction(bCtor) && (bCtor instanceof bCtor)) && + ('constructor' in a && 'constructor' in b)) { + return false; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + var size = 0, + result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = (deepEqual ? equals(a[size], b[size], aStack, bStack, deepEqual) : a[size] === b[size]))) { + break; + } + } + } + } else { + // Deep compare objects. + for (var key in a) { + if (blocks.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = blocks.has(b, key) && (deepEqual ? equals(a[key], b[key], aStack, bStack, deepEqual) : a[key] === b[key]))) { + break; + } + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (blocks.has(b, key) && !(size--)) { + break; + } + } + result = !size; + } + } + + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + } + + blocks.at = function (index) { + return { + index: index, + prototypeIndentification: '__blocks.at__' + }; + }; + + blocks.first = function () { + return blocks.first; + }; + + blocks.last = function () { + return blocks.last; + }; + + function _super(name, args) { + var Class = this.__Class__; + var result; + var func; + + if (blocks.isString(name)) { + func = Class.prototype[name]; + } else { + args = name; + func = Class; + } + + this.__Class__ = Class.prototype.__Class__; + result = func.apply(this, args || []); + this.__Class__ = Class; + + return result; + } + + var objectCreate = Object.create || function(prototype) { + var Class = function() {}; + Class.prototype = prototype; + return new Class(); + }; + + for (var key in [support]) { + break; + } + support.ownPropertiesAreLast = key != '0'; + + function parseCallback(callback, thisArg) { + if (thisArg != null) { + var orgCallback = callback; + callback = function(value, index, collection) { + return orgCallback.call(thisArg, value, index, collection); + }; + } + return callback; + } + + (function () { + +(function () { + +var customTypes = {}; + +blocks.debug = { + enabled: true, + + enable: function () { + blocks.debug.enabled = true; + }, + + disable: function () { + blocks.debug.enabled = false; + }, + + addType: function (name, checkCallback) { + customTypes[name.toLowerCase()] = checkCallback; + }, + + checkArgs: debugFunc(function (method, args, options) { + if (!blocks.debug.enabled) { + return; + } + if (!options) { + return; + } + var errors = checkMethod(method, args); + + if (errors.length === 0) { + return; + } + + blocks.debug.printErrors(method, args, options, errors); + }), + + printErrors: function (method, args, options, errors) { + if (!blocks.debug.enabled) { + return; + } + var message = new ConsoleMessage(); + var one = errors.length === 1; + var firstError = errors[0]; + + if (one) { + message.beginCollapsedGroup(); + } else { + message.beginGroup(); + } + + message + .addSpan('Arguments mismatch:', { background: 'yellow'}) + .addText(' '); + + if (one) { + addError(message, firstError, method, options.paramsNames); + tryInsertElement(message, options.element); + addMethodReference(message, method); + } else { + message.addText('Multiple errors:'); + tryInsertElement(message, options.element); + message.newLine(); + for (var i = 0; i < errors.length; i++) { + message.addText((i + 1) + '. '); + addError(message, errors[i], method, options.paramsNames); + message.newLine(); + } + message.beginCollapsedGroup(); + message.addText('Method reference'); + message.newLine(); + addMethodReference(message, method, true); + message.endGroup(); + } + + message.endGroup(); + message.print(); + }, + + checkQuery: function (name, args, query, element) { + if (!blocks.debug.enabled || name == 'if' || name == 'ifnot') { + return; + } + var method = blocks.debug.queries[name]; + if (method) { + blocks.debug.checkArgs(method, args, { + paramsNames: query.params, + element: element + }); + } + }, + + queryNotExists: function (query, element) { + if (!blocks.debug.enabled) { + return; + } + var message = blocks.debug.Message(); + message.beginSpan({ 'font-weight': 'bold' }); + message.addSpan('Warning:', { background: 'orange', padding: '0 3px' }); + + message.addText(' '); + message.addSpan(query.name, { background: 'red', color: 'white' }); + message.addSpan('(' + query.params.join(', ') + ')', { background: '#EEE' }); + + message.addText(' - data-query '); + message.addSpan(query.name, { background: '#EEE', padding: '0 5px' }); + message.addText(' does not exists'); + tryInsertElement(message, element); + message.endSpan(); + + message.print(); + }, + + queryParameterFail: function (query, failedParameter, element) { + if (!blocks.debug.enabled) { + return; + } + var method = blocks.debug.queries[query.name]; + var message = blocks.debug.Message(); + var params = query.params; + var param; + + if (!method) { + return; + } + + message.beginCollapsedGroup(); + message.beginSpan({ 'font-weight': 'bold' }); + message.addSpan('Critical:', { background: 'red', color: 'white' }); + message.addText(' '); + message.beginSpan({ background: '#EEE' }); + message.addText(query.name + '('); + for (var i = 0; i < params.length; i++) { + param = params[i]; + if (param == failedParameter) { + message.addSpan(param, { background: 'red', color: 'white' }); + } else { + message.addText(param); + } + if (i != params.length - 1) { + message.addText(', '); + } + } + message.addText(')'); + message.endSpan(); + message.addText(' - exception thrown while executing query parameter'); + tryInsertElement(message, element); + addMethodReference(message, method); + message.endGroup(); + + message.print(); + }, + + expressionFail: function (expressionText, element) { + if (!blocks.debug.enabled) { + return; + } + var message = new blocks.debug.Message(); + + message.beginSpan({ 'font-weight': 'bold' }); + message.addSpan('Critical:', { background: 'red', color: 'white' }); + message.addText(' '); + message.addSpan('{{' + expressionText + '}}', { background: 'red', color: 'white' }); + message.addText(' - exception thrown while executing expression'); + message.endSpan(); + + tryInsertElement(message, element); + + message.print(); + }, + + Message: ConsoleMessage +}; + +function tryInsertElement(message, element) { + if (element) { + //message.addText(' -->'); + message.addText(' '); + if (blocks.VirtualElement.Is(element)) { + if (element._el) { + message.addElement(element._el); + } else { + message.addText(element.renderBeginTag()); + } + } else { + message.addElement(element); + } + } +} + +function addMethodReference(message, method, examplesExpanded) { + var examples = method.examples; + + message + .newLine() + .addSpan(method.description, { color: 'green' }); + + addMethodSignature(message, method); + + if (examplesExpanded) { + message.beginGroup(); + } else { + message.beginCollapsedGroup(); + } + + message + .addSpan('Usage example' + (examples.length > 1 ? 's' : ''), { color: 'blue' }) + .newLine(); + + for (var i = 0; i < method.examples.length;i++) { + addCodeTree(message, method.examples[i].code); + } + + message.endGroup(); +} + +function addCodeTree(message, codeTree) { + var children = codeTree.children; + var lines; + var child; + + message.beginSpan(highlightjs[codeTree.name]); + + for (var i = 0; i < children.length; i++) { + child = children[i]; + + if (typeof child == 'string') { + message.addText(child.split('\n').join('\n ')); + } else { + addCodeTree(message, child); + } + } + + message.endSpan(); +} + +function addError(message, error, method, paramNames) { + var params = method.params; + var index; + + if (!paramNames) { + paramNames = []; + for (index = 0; index < params.length; index++) { + paramNames.push(params[index].name); + } + } + + message.beginSpan({ + 'background-color': '#EEE' + }); + + message.addText(method.name + '('); + + if (error) { + switch (error.type) { + case 'less-args': + message.addText(paramNames.slice(0, error.actual).join(', ')); + if (error.actual > 0) { + message.addText(', '); + } + for (index = 0; index < error.expected; index++) { + message + .beginSpan({ + 'background-color': 'red', + padding: '0 5px', + color: 'white' + }) + .addText('?') + .endSpan(); + if (index != error.expected - 1) { + message.addText(', '); + } + } + message.addText(')'); + message.endSpan(); + message.addText(' - less arguments than the required specified'); + break; + case 'more-args': + message.addText(paramNames.slice(0, error.expected).join(', ')); + if (error.expected > 0) { + message.addText(', '); + } + for (index = error.expected; index < error.actual; index++) { + message.addSpan(paramNames[index], { + 'background-color': 'red', + 'text-decoration': 'line-through', + color: 'white' + }); + if (index != error.actual - 1) { + message.addText(', '); + } + } + message.addText(')'); + message.endSpan(); + message.addText(' - ' + (error.actual - error.expected) + ' unnecessary arguments specified'); + break; + case 'param': + for (index = 0; index < paramNames.length; index++) { + if (method.params[index] == error.param) { + message.addSpan(paramNames[index], { + 'background-color': 'red', + color: 'white' + }); + } else { + message.addText(paramNames[index]); + } + if (index != paramNames.length - 1) { + message.addText(', '); + } + } + message.addText(')'); + message.endSpan(); + message.addText(' - ' + error.actual + ' specified where ' + error.expected + ' expected'); + break; + } + } else { + message.addText(')'); + message.endSpan(); + } +} + +function debugFunc(callback) { + return function () { + if (blocks.debug.executing) { + return; + } + blocks.debug.executing = true; + callback.apply(blocks.debug, blocks.toArray(arguments)); + blocks.debug.executing = false; + }; +} + +function checkMethod(method, args) { + var errors = []; + + errors = errors.concat(checkArgsCount(method, args)); + if (errors.length === 0 || errors[0].type == 'more-args') { + errors = errors.concat(checkArgsTypes(method, args)); + } + + return errors; +} + +function checkArgsCount(method, args) { + var errors = []; + var requiredCount = 0; + var hasArguments = false; + var params = method.params; + var param; + + for (var i = 0; i < params.length; i++) { + param = params[i]; + if (!param.optional) { + requiredCount++; + } + if (param.isArguments) { + hasArguments = true; + } + } + + if (args.length < requiredCount) { + errors.push({ + type: 'less-args', + actual: args.length, + expected: requiredCount + }); + } + if (!hasArguments && args.length > params.length) { + errors.push({ + type: 'more-args', + actual: args.length, + expected: params.length + }); + } + + return errors; +} + +function getOptionalParamsCount(params) { + var count = 0; + + for (var i = 0; i < params.length; i++) { + if (params[i].optional) { + count++; + } + } + + return count; +} + +function checkArgsTypes(method, args) { + var errors = []; + var params = method.params; + var maxOptionals = params.length - (params.length - getOptionalParamsCount(method.params)); + var paramIndex = 0; + var passDetailValues = blocks.queries[method.name].passDetailValues; + var currentErrors; + var param; + var value; + + for (var i = 0; i < args.length; i++) { + param = params[paramIndex]; + value = args[i]; + + if (!param) { + break; + } + + if (passDetailValues) { + value = value.rawValue; + } + + if (param.optional) { + if (maxOptionals > 0) { + currentErrors = checkType(param, value); + if (currentErrors.length === 0) { + maxOptionals -= 1; + if (!param.isArguments) { + paramIndex += 1; + } + } + } + } else { + errors = errors.concat(checkType(param, value)); + if (!param.isArguments) { + paramIndex += 1; + } + } + } + + return errors; +} + +function checkType(param, value) { + var unwrapedValue = blocks.unwrap(value); + var errors = []; + var types = param.types; + var satisfied = false; + var valueType; + var type; + + for (var i = 0; i < types.length; i++) { + type = types[i].toLowerCase(); + type = type.replace('...', ''); + + if (type == '*') { + satisfied = true; + break; + } + + if (type == 'falsy') { + if (!unwrapedValue) { + satisfied = true; + break; + } else { + continue; + } + } else if (type == 'truthy') { + if (unwrapedValue) { + satisfied = true; + break; + } else { + continue; + } + } else if (customTypes[type]) { + satisfied = customTypes[type](value); + if (satisfied) { + break; + } else { + continue; + } + } else if (blocks.isObservable(value)) { + valueType = 'blocks.observable()'; + } else { + valueType = blocks.type(value).toLowerCase(); + } + + if (type === valueType) { + satisfied = true; + break; + } else if (valueType == 'blocks.observable()') { + valueType = blocks.type(blocks.unwrapObservable(value)); + if (type === valueType) { + satisfied = true; + break; + } + } + } + + if (!satisfied) { + errors.push({ + type: 'param', + param: param, + actual: valueType, + expected: types + }); + } + + return errors; +} + +function addMethodSignature(message, method) { + var params = method.params; + var paramNames = []; + var index; + + message + .newLine() + .beginSpan({ 'font-size': '15px', 'font-weight': 'bold' }) + .addText(method.name + '('); + + for (index = 0; index < params.length; index++) { + paramNames.push(params[index].rawName); + } + message.addText(paramNames.join(', ') + ')'); + if (method.returns) { + message.addText(' returns ' + method.returns.types[0]); + if (method.returns.description) { + + } + } + + message.endSpan(); + + for (index = 0; index < params.length; index++) { + message + .newLine() + .addText(' ' + params[index].rawName + ' {' + params[index].types.join('|') + '} - ' + params[index].description); + } + + message.newLine(); +} + +function examples(method) { + var examples = method.examples; + + if (examples) { + console.groupCollapsed('%cUsage examples', 'color: blue;'); + + for (var i = 0; i < examples.length; i++) { + console.log(examples[i].code); + if (i != examples.length - 1) { + console.log('-------------------------------'); + } + } + + console.groupEnd(); + } +} + +function params(method) { + var params = method.params; + for (var i = 0; i < params.length; i++) { + console.log(' ' + method.params[i].name + ': ' + method.params[i].description); + } +} + +function ConsoleMessage() { + if (!ConsoleMessage.prototype.isPrototypeOf(this)) { + return new ConsoleMessage(); + } + this._rootSpan = { + styles: {}, + children: [], + parent: null + }; + this._currentSpan = this._rootSpan; +} + +ConsoleMessage.Support = (function () { + // https://github.com/jquery/jquery-migrate/blob/master/src/core.js + function uaMatch( ua ) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) || + /(webkit)[ \/]([\w.]+)/.exec( ua ) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) || + /(msie) ([\w.]+)/.exec( ua ) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + } + var browserData = uaMatch(navigator.userAgent); + + return { + isIE: browserData.browser == 'msie' || (browserData.browser == 'mozilla' && parseInt(browserData.version, 10) == 11) + }; +})(); + +ConsoleMessage.prototype = { + beginGroup: function () { + this._currentSpan.children.push({ + type: 'group', + parent: this._currentSpan + }); + return this; + }, + + beginCollapsedGroup: function () { + this._currentSpan.children.push({ + type: 'groupCollapsed' + }); + return this; + }, + + endGroup: function () { + this._currentSpan.children.push({ + type: 'groupEnd', + parent: this._currentSpan + }); + return this; + }, + + beginSpan: function (styles) { + var span = { + type: 'span', + styles: styles, + children: [], + parent: this._currentSpan + }; + this._currentSpan.children.push(span); + this._currentSpan = span; + return this; + }, + + endSpan: function () { + this._currentSpan = this._currentSpan.parent || this._currentSpan; + return this; + }, + + addSpan: function (text, styles) { + this.beginSpan(styles); + this.addText(text); + this.endSpan(); + return this; + }, + + addText: function (message) { + this._currentSpan.children.push({ + type: 'text', + message: message, + parent: this._currentSpan + }); + return this; + }, + + newLine: function (type) { + this._currentSpan.children.push({ + type: type || 'log', + parent: this._currentSpan + }); + return this; + }, + + addImage: function () { + (function () { + var faviconUrl = "http://d2c87l0yth4zbw.cloudfront.net/i/_global/favicon.png", + css = "background-image: url('" + faviconUrl + "');" + + "background-repeat: no-repeat;" + + "display: block;" + + "background-size: 13px 13px;" + + "padding-left: 13px;" + + "margin-left: 5px;", + text = "Do you like coding? Visit www.spotify.com/jobs"; + if (navigator.userAgent.match(/chrome/i)) { + console.log(text + '%c', css); + } else { + console.log('%c ' + text, css); + } + })(); + return this; + }, + + addElement: function (element) { + this._currentSpan.children.push({ + type: 'element', + element: element, + parent: this._currentSpan + }); + return this; + }, + + print: function () { + if (typeof console == 'undefined') { + return; + } + + var messages = [this._newMessage()]; + var message; + + this._printSpan(this._rootSpan, messages); + + for (var i = 0; i < messages.length; i++) { + message = messages[i]; + if (message.text && message.text != '%c' && console[message.type]) { + Function.prototype.apply.call(console[message.type], console, [message.text].concat(message.args)); + } + } + + return this; + }, + + _printSpan: function (span, messages) { + var children = span.children; + var message = messages[messages.length - 1]; + + this._addSpanData(span, message); + + for (var i = 0; i < children.length; i++) { + this._handleChild(children[i], messages); + } + }, + + _handleChild: function (child, messages) { + var message = messages[messages.length - 1]; + + switch (child.type) { + case 'group': + messages.push(this._newMessage('group')); + break; + case 'groupCollapsed': + messages.push(this._newMessage('groupCollapsed')); + break; + case 'groupEnd': + message = this._newMessage('groupEnd'); + message.text = ' '; + messages.push(message); + messages.push(this._newMessage()) + break; + case 'span': + this._printSpan(child, messages); + this._addSpanData(child, message); + this._addSpanData(child.parent, message); + break; + case 'text': + message.text += child.message; + break; + case 'element': + message.text += '%o'; + message.args.push(child.element); + break; + case 'log': + messages.push(this._newMessage(child.type)); + break; + } + }, + + _addSpanData: function (span, message) { + if (!ConsoleMessage.Support.isIE) { + if (message.text.substring(message.text.length - 2) == '%c') { + message.args[message.args.length - 1] = this._stylesString(span.styles); + } else { + message.text += '%c'; + message.args.push(this._stylesString(span.styles)); + } + } + }, + + _newMessage: function (type) { + return { + type: type || 'log', + text: '', + args: [] + }; + }, + + _stylesString: function (styles) { + var result = ''; + for (var key in styles) { + result += key + ':' + styles[key] + ';'; + } + return result; + } +}; + +var highlightjs = { + 'xml': {}, + 'hljs-tag': { + + }, + 'hljs-title': { + color: '#5cd94d' + }, + 'hljs-expression': { + color: '#7b521e' + }, + 'hljs-variable': { + color: '#7b521e' + }, + 'hljs-keyword': {}, + 'hljs-string': {}, + 'hljs-function': {}, + 'hljs-params': {}, + 'hljs-number': {}, + 'hljs-regexp': {}, + 'hljs-comment': { + color: '#888' + }, + 'hljs-attribute': { + color: '#2d8fd0' + }, + 'hljs-value': { + color: '#e7635f' + } +}; + +})(); +(function () { +blocks.debug.queries = { + 'if': { + fullName: 'if', + name: 'if', + description: 'Executes particular query depending on the condition specified', + params: [{ + name: 'condition', + rawName: 'condition', + types: ['boolean'], + optional: false, + isArguments: false, + description: 'The result will determine if the consequent or the alternate query will be executed', + defaultValue: '', + value: '' + }, { + name: 'consequent', + rawName: 'consequent', + types: ['data-query'], + optional: false, + isArguments: false, + description: 'The query that will be executed if the specified condition returns a truthy value', + defaultValue: '', + value: '' + }, { + name: 'alternate', + rawName: '[alternate]', + types: ['data-query'], + optional: true, + isArguments: false, + description: 'The query that will be executed if the specified condition returns a falsy value', + defaultValue: '', + value: '' + }], + returns: undefined, + examples: [{ + language: 'html', + code: { + name: '', + children: [{ + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"if(true, setClass(\'success\'), setClass(\'fail\'))"'] + }, '>'] + }, { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"if(false, setClass(\'success\'), setClass(\'fail\'))"'] + }, '>'] + }, { + name: 'hljs-tag', + children: [''] + }, '\n\n', { + name: 'hljs-comment', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"if(true, setClass(\'success\'), setClass(\'fail\'))"'] + }, ' ', { + name: 'hljs-attribute', + children: ['class'] + }, '=', { + name: 'hljs-value', + children: ['"success"'] + }, '>'] + }, { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"if(false, setClass(\'success\'), setClass(\'fail\'))"'] + }, ' ', { + name: 'hljs-attribute', + children: ['class'] + }, '=', { + name: 'hljs-value', + children: ['"fail"'] + }, '>'] + }, { + name: 'hljs-tag', + children: [''] + }] + }, + value: '' + }], + memberof: 'blocks.queries' + }, + ifnot: { + fullName: 'ifnot', + name: 'ifnot', + description: 'Executes particular query depending on the condition specified.\n The opposite query of the \'if\'', + params: [{ + name: 'condition', + rawName: 'condition', + types: ['boolean'], + optional: false, + isArguments: false, + description: 'The result will determine if the consequent or the alternate query will be executed', + defaultValue: '', + value: '' + }, { + name: 'consequent', + rawName: 'consequent', + types: ['data-query'], + optional: false, + isArguments: false, + description: 'The query that will be executed if the specified condition returns a falsy value', + defaultValue: '', + value: '' + }, { + name: 'alternate', + rawName: '[alternate]', + types: ['data-query'], + optional: true, + isArguments: false, + description: 'The query that will be executed if the specified condition returns a truthy value', + defaultValue: '', + value: '' + }], + returns: undefined, + examples: [{ + language: 'html', + code: { + name: '', + children: [{ + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"ifnot(true, setClass(\'success\'), setClass(\'fail\'))"'] + }, '>'] + }, { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"ifnot(false, setClass(\'success\'), setClass(\'fail\'))"'] + }, '>'] + }, { + name: 'hljs-tag', + children: [''] + }, '\n\n', { + name: 'hljs-comment', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"ifnot(true, setClass(\'success\'), setClass(\'fail\'))"'] + }, ' ', { + name: 'hljs-attribute', + children: ['class'] + }, '=', { + name: 'hljs-value', + children: ['"fail"'] + }, '>'] + }, { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"ifnot(false, setClass(\'success\'), setClass(\'fail\'))"'] + }, ' ', { + name: 'hljs-attribute', + children: ['class'] + }, '=', { + name: 'hljs-value', + children: ['"success"'] + }, '>'] + }, { + name: 'hljs-tag', + children: [''] + }] + }, + value: '' + }], + memberof: 'blocks.queries' + }, + template: { + fullName: 'template', + name: 'template', + description: 'Queries and sets the inner html of the element from the template specified', + params: [{ + name: 'template', + rawName: 'template', + types: ['HTMLElement', 'string'], + optional: false, + isArguments: false, + description: 'The template that will be rendered\n The value could be an element id (the element innerHTML property will be taken), string (the template) or\n an element (again the element innerHTML property will be taken)', + defaultValue: '', + value: '' + }, { + name: 'value', + rawName: '[value]', + types: ['*'], + optional: true, + isArguments: false, + description: 'Optional context for the template', + defaultValue: '', + value: '' + }], + returns: undefined, + examples: [{ + language: 'html', + code: { + name: '', + children: [{ + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['script'] + }, '>'] + }, { + name: 'less', + children: ['\n ', { + name: 'hljs-tag', + children: ['blocks'] + }, { + name: 'hljs-class', + children: ['.query'] + }, '({\n ', { + name: 'hljs-attribute', + children: ['name'] + }, ': ', { + name: 'hljs-string', + children: ['\'John Doe\''] + }, ',\n ', { + name: 'hljs-attribute', + children: ['age'] + }, ': ', { + name: 'hljs-number', + children: ['22'] + }, '\n });\n'] + }, { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['script'] + }, ' ', { + name: 'hljs-attribute', + children: ['id'] + }, '=', { + name: 'hljs-value', + children: ['"user"'] + }, ' ', { + name: 'hljs-attribute', + children: ['type'] + }, '=', { + name: 'hljs-value', + children: ['"blocks-template"'] + }, '>'] + }, { + name: 'handlebars', + children: [{ + name: 'xml', + children: ['\n ', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['h3'] + }, '>'] + }] + }, { + name: 'hljs-expression', + children: ['{{', { + name: 'hljs-variable', + children: ['name'] + }, '}}'] + }, { + name: 'xml', + children: [{ + name: 'hljs-tag', + children: [''] + }, '\n ', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['p'] + }, '>'] + }, 'I am '] + }, { + name: 'hljs-expression', + children: ['{{', { + name: 'hljs-variable', + children: ['age'] + }, '}}'] + }, { + name: 'xml', + children: [' years old.', { + name: 'hljs-tag', + children: [''] + }, '\n'] + }] + }, { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"template(\'user\')"'] + }, '>'] + }, '\n', { + name: 'hljs-tag', + children: [''] + }, '\n\n', { + name: 'hljs-comment', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"template(\'user\')"'] + }, '>'] + }, '\n ', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['h3'] + }, '>'] + }, 'John Doe', { + name: 'hljs-tag', + children: [''] + }, '\n ', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['p'] + }, '>'] + }, 'I am 22 years old.', { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: [''] + }] + }, + value: '' + }], + memberof: 'blocks.queries' + }, + define: { + fullName: 'define', + name: 'define', + description: 'Creates a variable name that could be used in child elements', + params: [{ + name: 'propertyName', + rawName: 'propertyName', + types: ['string'], + optional: false, + isArguments: false, + description: 'The name of the value that will be\n created and you could access its value later using that name', + defaultValue: '', + value: '' + }, { + name: 'propertyValue', + rawName: 'propertyValue', + types: ['*'], + optional: false, + isArguments: false, + description: 'The value that the property will have', + defaultValue: '', + value: '' + }], + returns: undefined, + examples: [{ + language: 'html', + code: { + name: '', + children: [{ + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['script'] + }, '>'] + }, { + name: 'less', + children: ['\n ', { + name: 'hljs-tag', + children: ['blocks'] + }, { + name: 'hljs-class', + children: ['.query'] + }, '({\n ', { + name: 'hljs-tag', + children: ['strings'] + }, ': {\n ', { + name: 'hljs-tag', + children: ['title'] + }, ': {\n ', { + name: 'hljs-attribute', + children: ['text'] + }, ': ', { + name: 'hljs-string', + children: ['\'Hello World!\''] + }, '\n }\n }\n });\n'] + }, { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"define(\'$title\', strings.title.text)"'] + }, '>'] + }, '\n The title is {{$title}}.\n', { + name: 'hljs-tag', + children: [''] + }, '\n\n', { + name: 'hljs-comment', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"define(\'$title\', strings.title.text)"'] + }, '>'] + }, '\n The title is Hello World!.\n', { + name: 'hljs-tag', + children: [''] + }] + }, + value: '' + }], + memberof: 'blocks.queries' + }, + 'with': { + fullName: 'with', + name: 'with', + description: 'Changes the current context for the child elements.\n Useful when you will work a lot with a particular value', + params: [{ + name: 'value', + rawName: 'value', + types: ['*'], + optional: false, + isArguments: false, + description: 'The new context', + defaultValue: '', + value: '' + }, { + name: 'name', + rawName: '[name]', + types: ['string'], + optional: true, + isArguments: false, + description: 'Optional name of the new context\n This way the context will also available under the name not only under the $this context property', + defaultValue: '', + value: '' + }], + returns: undefined, + examples: [{ + language: 'html', + code: { + name: '', + children: [{ + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['script'] + }, '>'] + }, { + name: 'less', + children: ['\n ', { + name: 'hljs-tag', + children: ['blocks'] + }, { + name: 'hljs-class', + children: ['.query'] + }, '({\n ', { + name: 'hljs-tag', + children: ['ProfilePage'] + }, ': {\n ', { + name: 'hljs-tag', + children: ['user'] + }, ': {\n ', { + name: 'hljs-attribute', + children: ['name'] + }, ': ', { + name: 'hljs-string', + children: ['\'John Doe\''] + }, ',\n ', { + name: 'hljs-attribute', + children: ['age'] + }, ': ', { + name: 'hljs-number', + children: ['22'] + }, '\n }\n }\n });\n'] + }, { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"with(ProfilePage.user, \'$user\')"'] + }, '>'] + }, '\n My name is {{$user.name}} and I am {{$this.age}} years old.\n', { + name: 'hljs-tag', + children: [''] + }, '\n\n', { + name: 'hljs-comment', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['div'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"with(ProfilePage.user, \'$user\')"'] + }, '>'] + }, '\n My name is John Doe and I am 22 years old.\n', { + name: 'hljs-tag', + children: [''] + }] + }, + value: '' + }], + memberof: 'blocks.queries' + }, + each: { + fullName: 'each', + name: 'each', + description: 'The each method iterates through an array items or object values\n and repeats the child elements by using them as a template', + params: [{ + name: 'collection', + rawName: 'collection', + types: ['Array', 'Object'], + optional: false, + isArguments: false, + description: 'The collection to iterate over', + defaultValue: '', + value: '' + }], + returns: undefined, + examples: [{ + language: 'html', + code: { + name: '', + children: [{ + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['script'] + }, '>'] + }, { + name: 'css', + children: ['\n ', { + name: 'hljs-tag', + children: ['blocks'] + }, { + name: 'hljs-class', + children: ['.query'] + }, '(', { + name: 'hljs-rules', + children: ['{\n ', { + name: 'hljs-rule', + children: [{ + name: 'hljs-attribute', + children: ['items'] + }, ':', { + name: 'hljs-value', + children: [' [', { + name: 'hljs-string', + children: ['\'John\''] + }, ', ', { + name: 'hljs-string', + children: ['\'Doe\''] + }, ']\n })'] + }] + }, ';\n'] + }] + }, { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['ul'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"each(items)"'] + }, '>'] + }, '\n ', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['li'] + }, '>'] + }, '{{$this}}', { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: [''] + }, '\n\n', { + name: 'hljs-comment', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['ul'] + }, ' ', { + name: 'hljs-attribute', + children: ['data-query'] + }, '=', { + name: 'hljs-value', + children: ['"each(items)"'] + }, '>'] + }, '\n ', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['li'] + }, '>'] + }, 'John', { + name: 'hljs-tag', + children: [''] + }, '\n ', { + name: 'hljs-tag', + children: ['<', { + name: 'hljs-title', + children: ['li'] + }, '>'] + }, 'Doe', { + name: 'hljs-tag', + children: [''] + }, '\n', { + name: 'hljs-tag', + children: [''] + }] + }, + value: '' + }], + memberof: 'blocks.queries' + }, + options: { + fullName: 'options', + name: 'options', + description: 'Render options for a element by providing an collection. + * + * @memberof blocks.queries + * @param {(Array|Object)} collection - The collection to iterate over + * @param {Object} [options] - Options to customize the behavior for creating each option. + * options.value - determines the field in the collection to server for the option value + * options.text - determines the field in the collection to server for the option text + * options.caption - creates a option with the specified text at the first option + * + * @example {html} + * + * + * + * + * + */ + options: { + passDomQuery: true, + + passRawValues: true, + + preprocess: function (domQuery, collection, options) { + options = options || {}; + var $thisStr = '$this'; + var text = Expression.Create('{{' + (options.text || $thisStr) + '}}'); + var value = Expression.Create('{{' + (options.value || $thisStr) + '}}', 'value'); + var caption = blocks.isString(options.caption) && new VirtualElement('option'); + var option = new VirtualElement('option'); + var children = this._children; + var i = 0; + var child; + + for (; i < children.length; i++) { + child = children[i]; + if (!child._attributes || (child._attributes && !child._attributes['data-role'])) { + children.splice(i--, 1); + } + } + + option._attributeExpressions.push(value); + option._children.push(text); + option._parent = this; + this._children.push(option); + + if (caption) { + caption._attributes['data-role'] = 'header'; + caption._innerHTML = options.caption; + this.addChild(caption); + } + + blocks.queries.each.preprocess.call(this, domQuery, collection); + } + }, + + /** + * The render query allows elements to be skipped from rendering and not to exist in the HTML result + * + * @memberof blocks.queries + * @param {boolean} condition - The value determines if the element will be rendered or not + * @param {boolean} [renderChildren=false] - The value indicates if the children will be rendered + * + * @example {html} + *
Visible
+ *
Invisible
+ * + * + *
Visible
+ */ + render: { + passDetailValues: true, + + preprocess: function (condition) { + if (!this._each && !this._sync) { + throw new Error('render() is supported only() in each context'); + } + + this._renderMode = condition.value ? VirtualElement.RenderMode.All : VirtualElement.RenderMode.None; + + if (condition.containsObservable && this._renderMode == VirtualElement.RenderMode.None) { + this._renderMode = VirtualElement.RenderMode.ElementOnly; + this.css('display', 'none'); + ElementsData.data(this, 'renderCache', this); + } + }, + + update: function (condition) { + var elementData = ElementsData.data(this); + if (elementData.renderCache && condition.value) { + // TODO: Should use the logic from dom.html method + this.innerHTML = elementData.renderCache.renderChildren(blocks.domQuery(this)); + blocks.domQuery(this).createElementObservableDependencies(this.childNodes); + elementData.renderCache = null; + } + + this.style.display = condition.value ? '' : 'none'; + } + }, + + /** + * Determines when an observable value will be synced from the DOM. + * Only applicable when using the 'val' data-query. + * + * @param {string} eventName - the name of the event. Possible values are: + * 'input'(default) + * 'keydown' - + * 'change' - + * 'keyup' - + * 'keypress' - + */ + updateOn: { + preprocess: function (eventName) { + ElementsData.data(this).updateOn = eventName; + } + }, + + /** + * Could be used for custom JavaScript animation by providing a callback function + * that will be called the an animation needs to be performed + * + * @memberof blocks.queries + * @param {Function} callback - The function that will be called when animation needs + * to be performed. + * + * @example {html} + * + * + *
+ *
+ */ + animate: { + preprocess: function (callback) { + ElementsData.data(this).animateCallback = callback; + } + }, + + /** + * Adds or removes a class from an element + * + * @memberof blocks.queries + * @param {string|Array} className - The class string (or array of strings) that will be added or removed from the element. + * @param {boolean|undefined} [condition=true] - Optional value indicating if the class name will be added or removed. true - add, false - remove. + * + * @example {html} + *
+ * + * + *
+ */ + setClass: { + preprocess: function (className, condition) { + if (arguments.length > 1) { + this.toggleClass(className, !!condition); + } else { + this.addClass(className); + } + }, + + update: function (className, condition) { + var virtual = ElementsData.data(this).virtual; + if (virtual._each) { + virtual = VirtualElement(); + virtual._el = this; + } + if (arguments.length > 1) { + virtual.toggleClass(className, condition); + } else { + virtual.addClass(className); + } + } + }, + + /** + * Sets the inner html to the element + * + * @memberof blocks.queries + * @param {string} html - The html that will be places inside element replacing any other content. + * @param {boolean} [condition=true] - Condition indicating if the html will be set. + * + * @example {html} + *
+ * + * + *
some content
+ */ + html: { + call: true + }, + + /** + * Adds or removes the inner text from an element. Escapes any HTML provided + * + * @memberof blocks.queries + * @param {string} text - The text that will be places inside element replacing any other content. + * @param {boolean} [condition=true] - Value indicating if the text will be added or cleared. true - add, false - clear. + * + * @example {html} + *
+ * + * + *
some content
+ */ + text: { + call: true + }, + + /** + * Determines if an html element will be visible. Sets the CSS display property. + * + * @memberof blocks.queries + * @param {boolean} [condition=true] Value indicating if element will be visible or not. + * + * @example {html} + *
Visible
+ *
Invisible
+ * + * + *
Visible
+ *
Invisible
+ */ + visible: { + call: 'css', + + prefix: 'display' + }, + + /** + * Gets, sets or removes an element attribute. + * Passing only the first parameter will return the attributeName value + * + * @memberof blocks.queries + * @param {string} attributeName - The attribute name that will be get, set or removed. + * @param {string} attributeValue - The value of the attribute. It will be set if condition is true. + * + * @example {html} + *
+ * + * + *
+ */ + attr: { + passRawValues: true, + + call: true + }, + + /** + * Sets the value attribute on an element. + * + * @memberof blocks.queries + * @param {(string|number|Array|undefined)} value - The new value for the element. + * + * @example {html} + * + * + * + * + * + */ + val: { + passRawValues: true, + + call: 'attr', + + prefix: 'value' + }, + + /** + * Sets the checked attribute on an element + * + * @memberof blocks.queries + * @param {boolean|undefined} [condition=true] - Determines if the element will be checked or not + * + * @example {html} + * + * + * + * + * + * + */ + checked: { + passRawValues: true, + + call: 'attr' + }, + + /** + * Sets the disabled attribute on an element + * + * @memberof blocks.queries + * @param {boolean|undefined} [condition=true] - Determines if the element will be disabled or not + */ + disabled: { + passRawValues: true, + + call: 'attr' + }, + + /** + * Gets, sets or removes a CSS style from an element. + * Passing only the first parameter will return the CSS propertyName value. + * + * @memberof blocks.queries + * @param {string} name - The name of the CSS property that will be get, set or removed + * @param {string} value - The value of the of the attribute. It will be set if condition is true + * + * @example {html} + * + *

+ *

+ * + * + *

+ *

+ */ + css: { + call: true + }, + + /** + * Sets the width of the element + * + * @memberof blocks.queries + * @param {(number|string)} value - The new width of the element + */ + width: { + call: 'css' + }, + + /** + * Sets the height of the element + * + * @memberof blocks.queries + * @param {number|string} value - The new height of the element + */ + height: { + call: 'css' + }, + + focused: { + preprocess: blocks.noop, + + update: function (value) { + if (value) { + this.focus(); + } + } + }, + + /** + * Subscribes to an event + * + * @memberof blocks.queries + * @param {(String|Array)} events - The event or events to subscribe to + * @param {Function} callback - The callback that will be executed when the event is fired + * @param {*} [args] - Optional arguments that will be passed as second parameter to + * the callback function after the event arguments + */ + on: { + ready: function (events, callbacks, args) { + if (!events || !callbacks) { + return; + } + var element = this; + var context = blocks.context(this); + var thisArg; + + callbacks = blocks.toArray(callbacks); + + var handler = function (e) { + context = blocks.context(this) || context; + thisArg = context.$template || context.$view || context.$root; + blocks.each(callbacks, function (callback) { + callback.call(thisArg, e, args); + }); + }; + + events = blocks.isArray(events) ? events : events.toString().split(' '); + + blocks.each(events, function (event) { + addListener(element, event, handler); + }); + } + } + }); + + blocks.each([ + // Mouse + 'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mousemove', 'mouseout', + // HTML form + 'select', 'change', 'submit', 'reset', 'focus', 'blur', + // Keyboard + 'keydown', 'keypress', 'keyup' + ], function (eventName) { + blocks.queries[eventName] = { + passRawValues: true, + + ready: function (callback, data) { + blocks.queries.on.ready.call(this, eventName, callback, data); + } + }; + }); + + var OBSERVABLE = '__blocks.observable__'; + + + function ChunkManager(observable) { + this.observable = observable; + this.chunkLengths = {}; + this.dispose(); + } + + ChunkManager.prototype = { + dispose: function () { + this.childNodesCount = undefined; + this.startIndex = 0; + this.observableLength = undefined; + this.startOffset = 0; + this.endOffset = 0; + }, + + setStartIndex: function (index) { + this.startIndex = index + this.startOffset; + }, + + setChildNodesCount: function (count) { + if (this.childNodesCount === undefined) { + this.observableLength = this.observable.__value__.length; + } + this.childNodesCount = count - (this.startOffset + this.endOffset); + }, + + chunkLength: function (wrapper) { + var chunkLengths = this.chunkLengths; + var id = ElementsData.id(wrapper); + var length = chunkLengths[id] || (this.childNodesCount || wrapper.childNodes.length) / (this.observableLength || this.observable.__value__.length); + var result; + + if (blocks.isNaN(length) || length === Infinity) { + result = 0; + } else { + result = Math.round(length); + } + + chunkLengths[id] = result; + + return result; + }, + + getAt: function (wrapper, index) { + var chunkLength = this.chunkLength(wrapper); + var childNodes = wrapper.childNodes; + var result = []; + + for (var i = 0; i < chunkLength; i++) { + result[i] = childNodes[index * chunkLength + i + this.startIndex]; + } + return result; + }, + + insertAt: function (wrapper, index, chunk) { + animation.insert( + wrapper, + this.chunkLength(wrapper) * index + this.startIndex, + blocks.isArray(chunk) ? chunk : [chunk]); + }, + + remove: function (index, howMany) { + var _this = this; + + this.each(function (domElement) { + blocks.context(domElement).childs.splice(index, howMany); + + for (var j = 0; j < howMany; j++) { + _this._removeAt(domElement, index); + } + }); + + ElementsData.collectGarbage(); + + this.dispose(); + + this.observable._indexes.splice(index, howMany); + }, + + add: function (addItems, index) { + var _this = this; + var observable = this.observable; + + blocks.each(addItems, function (item, i) { + observable._indexes.splice(index + i, 0, blocks.observable(index + i)); + }); + + this.each(function (domElement, virtualElement) { + var domQuery = blocks.domQuery(domElement); + var context = blocks.context(domElement); + var html = ''; + var syncIndex; + + domQuery.contextBubble(context, function () { + syncIndex = domQuery.getSyncIndex(); + for (var i = 0; i < addItems.length; i++) { + domQuery.dataIndex(blocks.observable.getIndex(observable, i + index, true)); + context.childs.splice(i + index, 0, domQuery.pushContext(addItems[i])); + html += virtualElement.renderChildren(domQuery, syncIndex + (i + index)); + domQuery.popContext(); + domQuery.dataIndex(undefined); + } + }); + + if (domElement.childNodes.length === 0) { + dom.html(domElement, html); + domQuery.createElementObservableDependencies(domElement.childNodes); + } else { + var fragment = domQuery.createFragment(html); + _this.insertAt(domElement, index, fragment); + } + }); + + this.dispose(); + }, + + each: function (callback) { + var i = 0; + var domElements = this.observable._elements; + + for (; i < domElements.length; i++) { + var data = domElements[i]; + if (!data.element) { + data.element = ElementsData.data(data.elementId).dom; + } + this.setup(data.element, callback); + } + }, + + setup: function (domElement, callback) { + if (!domElement) { + return; + } + + var eachData = ElementsData.data(domElement).eachData; + var element; + var commentId; + var commentIndex; + var commentElement; + + if (!eachData || eachData.id != this.observable.__id__) { + return; + } + + element = eachData.element; + this.startOffset = eachData.startOffset; + this.endOffset = eachData.endOffset; + + if (domElement.nodeType == 1) { + // HTMLElement + this.setStartIndex(0); + this.setChildNodesCount(domElement.childNodes.length); + callback(domElement, element, domElement); + } else { + // Comment + commentId = ElementsData.id(domElement); + commentElement = domElement.parentNode.firstChild; + commentIndex = 0; + while (commentElement != domElement) { + commentElement = commentElement.nextSibling; + commentIndex++; + } + this.setStartIndex(commentIndex + 1); + while (commentElement && (commentElement.nodeType != 8 || commentElement.nodeValue.indexOf(commentId + ':/blocks') != 1)) { + commentElement = commentElement.nextSibling; + commentIndex++; + } + this.setChildNodesCount(commentIndex - this.startIndex); + callback(domElement.parentNode, element, domElement); + } + }, + + _removeAt: function (wrapper, index) { + var chunkLength = this.chunkLength(wrapper); + + animation.remove( + wrapper, + chunkLength * index + this.startIndex, + chunkLength); + } + }; + + + + var observableId = 1; + + /** + * @namespace blocks.observable + * @param {*} initialValue - + * @param {*} [context] - + * @returns {blocks.observable} + */ + blocks.observable = function (initialValue, thisArg) { + var observable = function (value) { + if (arguments.length === 0) { + Events.trigger(observable, 'get', observable); + } + + var currentValue = getObservableValue(observable); + var update = observable.update; + + if (arguments.length === 0) { + Observer.registerObservable(observable); + return currentValue; + } else if (!blocks.equals(value, currentValue, false) && Events.trigger(observable, 'changing', value, currentValue) !== false) { + observable.update = blocks.noop; + if (!observable._dependencyType) { + if (blocks.isArray(currentValue) && blocks.isArray(value) && observable.reset) { + observable.reset(value); + } else { + observable.__value__ = value; + } + } else if (observable._dependencyType == 2) { + observable.__value__.set.call(observable.__context__, value); + } + + observable.update = update; + observable.update(); + + Events.trigger(observable, 'change', value, currentValue); + } + return observable; + }; + + initialValue = blocks.unwrap(initialValue); + + blocks.extend(observable, blocks.observable.fn.base); + observable.__id__ = observableId++; + observable.__value__ = initialValue; + observable.__context__ = thisArg || blocks.__viewInInitialize__ || observable; + observable._expressionKeys = {}; + observable._expressions = []; + observable._elementKeys = {}; + observable._elements = []; + + if (blocks.isArray(initialValue)) { + blocks.extend(observable, blocks.observable.fn.array); + observable._indexes = []; + observable._chunkManager = new ChunkManager(observable); + } else if (blocks.isFunction(initialValue)) { + observable._dependencyType = 1; // Function dependecy + } else if (initialValue && !initialValue.__Class__ && blocks.isFunction(initialValue.get) && blocks.isFunction(initialValue.set)) { + observable._dependencyType = 2; // Custom object + } + + updateDependencies(observable); + + return observable; + }; + + function updateDependencies(observable) { + if (observable._dependencyType) { + observable._getDependency = blocks.bind(getDependency, observable); + observable.on('get', observable._getDependency); + } + } + + function getDependency() { + var observable = this; + var value = observable.__value__; + var accessor = observable._dependencyType == 1 ? value : value.get; + + Events.off(observable, 'get', observable._getDependency); + observable._getDependency = undefined; + + Observer.startObserving(); + accessor.call(observable.__context__); + blocks.each(Observer.stopObserving(), function (dependency) { + var dependencies = (dependency._dependencies = dependency._dependencies || []); + var exists = false; + blocks.each(dependencies, function (value) { + if (observable === value) { + exists = true; + return false; + } + }); + if (!exists) { + dependencies.push(observable); + } + }); + } + + function getObservableValue(observable) { + var context = observable.__context__; + return observable._dependencyType == 1 ? observable.__value__.call(context) + : observable._dependencyType == 2 ? observable.__value__.get.call(context) + : observable.__value__; + } + + var observableIndexes = {}; + + blocks.extend(blocks.observable, { + getIndex: function (observable, index, forceGet) { + if (!blocks.isObservable(observable)) { + if (!observableIndexes[index]) { + observableIndexes[index] = blocks.observable(index); + } + return observableIndexes[index]; + } + var indexes = observable._indexes; + var $index; + + if (indexes) { + if (indexes.length == observable.__value__.length || forceGet) { + $index = indexes[index]; + } else { + $index = blocks.observable(index); + indexes.splice(index, 0, $index); + } + } else { + $index = blocks.observable(index); + } + + return $index; + }, + + fn: { + base: { + __identity__: OBSERVABLE, + + /** + * Updates all elements, expressions and dependencies where the observable is used + * + * @memberof blocks.observable + * @returns {blocks.observable} Returns the observable itself - return this; + */ + update: function () { + var elements = this._elements; + var elementData; + var domQuery; + var context; + var element; + var offset; + var value; + var isProperty; + var propertyName; + + blocks.eachRight(this._expressions, function updateExpression(expression) { + element = expression.element; + context = expression.context; + + if (!element) { + elementData = ElementsData.data(expression.elementId); + element = expression.element = elementData.dom; + } + + try { + value = blocks.unwrap(parameterQueryCache[expression.expression](context)); + } catch (ex) { + value = ''; + } + + value = value == null ? '' : value.toString(); + + offset = expression.length - value.length; + expression.length = value.length; + + isProperty = dom.props[expression.attr]; + propertyName = expression.attr ? dom.propFix[expression.attr.toLowerCase()] || expression.attr : null; + + if (element) { + if (expression.attr) { + if(isProperty) { + element[propertyName] = Expression.GetValue(context, null, expression.entire); + } else { + element.setAttribute(expression.attr, Expression.GetValue(context, null, expression.entire)); + } + } else { + if (element.nextSibling) { + element = element.nextSibling; + element.nodeValue = value + element.nodeValue.substring(expression.length + offset); + } else { + element.parentNode.appendChild(document.createTextNode(value)); + } + } + } else { + element = elementData.virtual; + if (expression.attr) { + element.attr(expression.attr, Expression.GetValue(context, null, expression.entire)); + } + } + }); + + for (var i = 0; i < elements.length; i++) { + value = elements[i]; + element = value.element; + if (!element && ElementsData.data(value.elementId)) { + element = value.element = ElementsData.data(value.elementId).dom; + if (!element) { + element = ElementsData.data(value.elementId).virtual; + } + } + if (VirtualElement.Is(element) || document.body.contains(element)) { + domQuery = blocks.domQuery(element); + domQuery.contextBubble(value.context, function () { + domQuery.executeMethods(element, value.cache); + }); + } else { + elements.splice(i, 1); + i -= 1; + } + } + + blocks.each(this._dependencies, function updateDependency(dependency) { + updateDependencies(dependency); + dependency.update(); + }); + + blocks.each(this._indexes, function updateIndex(observable, index) { + observable(index); + }); + + return this; + }, + + + on: function (eventName, callback, thisArg) { + Events.on(this, eventName, callback, thisArg || this.__context__); + return this; + }, + + once: function (eventName, callback, thisArg) { + Events.once(this, eventName, callback, thisArg || this.__context__); + return this; + }, + + off: function (eventName, callback) { + Events.off(this, eventName, callback); + return this; + }, + + /** + * Extends the current observable with particular functionality depending on the parameters + * specified. If the method is called without arguments and jsvalue framework is included + * the observable will be extended with the methods available in jsvalue for the current type + * + * @memberof blocks.observable + * @param {String} [name] - + * @param {...*} [options] + * @returns {*} - The result of the extend or the observable itself + * + * @example {javascript} + * blocks.observable.formatter = function () { + * // your code here + * }; + * + * // extending using the formatter extender + * var data = blocks.observable([1, 2, 3]).extend('formatter'); + * + */ + extend: function (name /*, options*/) { + var extendFunc = blocks.observable[name]; + var result; + + if (arguments.length === 0) { + if (blocks.core.expressionsCreated) { + blocks.core.applyExpressions(blocks.type(this()), this); + } + return this; + } else if (extendFunc) { + result = extendFunc.apply(this, blocks.toArray(arguments).slice(1)); + return blocks.isObservable(result) ? result : this; + } + }, + + clone: function (cloneValue) { + var value = this.__value__; + return blocks.observable(cloneValue ? blocks.clone(value) : value, this.__context__); + }, + + toString: function () { + var context = this.__context__; + var value = this._dependencyType == 1 ? this.__value__.call(context) + : this._dependencyType == 2 ? this.__value__.get.call(context) + : this.__value__; + + Observer.registerObservable(this); + + if (value != null && blocks.isFunction(value.toString)) { + return value.toString(); + } + return String(value); + } + }, + + /** + * @memberof blocks.observable + * @class array + */ + array: { + + /** + * Removes all items from the collection and replaces them with the new value provided. + * The value could be Array, observable array or jsvalue.Array + * + * @memberof array + * @param {Array} value - The new value that will be populated + * @returns {blocks.observable} - Returns the observable itself - return this; + * + * @example {javascript} + * // creates an observable array with [1, 2, 3] as values + * var items = blocks.observable([1, 2, 3]); + * + * // removes the previous values and fills the observable array with [5, 6, 7] values + * items.reset([5, 6, 7]); + */ + reset: function (array) { + if (arguments.length === 0) { + this.removeAll(); + return this; + } + + array = blocks.unwrap(array); + + var current = this.__value__; + var chunkManager = this._chunkManager; + var addCount = Math.max(array.length - current.length, 0); + var removeCount = Math.max(current.length - array.length, 0); + var updateCount = array.length - addCount; + + Events.trigger(this, 'removing', { + type: 'removing', + items: current, + index: 0 + }); + + Events.trigger(this, 'adding', { + type: 'adding', + items: array, + index: 0 + }); + + chunkManager.each(function (domElement, virtualElement) { + var domQuery = blocks.domQuery(domElement); + + domQuery.contextBubble(blocks.context(domElement), function () { + virtualElement.updateChildren(array, updateCount, domQuery, domElement); + }); + }); + + if (addCount > 0) { + chunkManager.add(array.slice(current.length), current.length); + } else if (removeCount > 0) { + chunkManager.remove(array.length, removeCount); + } + + this.__value__ = array; + + Events.trigger(this, 'remove', { + type: 'remove', + items: current, + index: 0 + }); + + Events.trigger(this, 'add', { + type: 'add', + items: array, + index: 0 + }); + + return this; + }, + + /** + * Adds values to the end of the observable array + * + * @memberof array + * @param {*} value - The values that will be added to the end of the array + * @param {number} [index] - Optional index specifying where to insert the value + * @returns {blocks.observable} - Returns the observable itself - return this; + * + * @example {javascript} + * var items = blocks.observable([1, 2, 3]); + * + * // results in observable array with [1, 2, 3, 4] values + * items.add(4); + * + */ + add: function (value, index) { + this.splice(blocks.isNumber(index) ? index : this.__value__.length, 0, value); + + return this; + }, + + /** + * Adds the values from the provided array(s) to the end of the collection + * + * @memberof array + * @param {Array} value - The array that will be added to the end of the array + * @param {number} [index] - Optional position where the array of values to be inserted + * @returns {blocks.observable} - Returns the observable itself - return this; + * + * @example {javascript} + * var items = blocks.observable([1, 2, 3]); + * + * // results in observable array with [1, 2, 3, 4, 5, 6] values + * items.addMany([4, 5], [6]); + */ + addMany: function (value, index) { + this.splice.apply(this, [blocks.isNumber(index) ? index : this.__value__.length, 0].concat(blocks.toArray(value))); + return this; + }, + + /** + * Swaps two values in the observable array. + * Note: Faster than removing the items and adding them at the locations + * + * @memberof array + * @param {number} indexA - The first index that points to the index in the array that will be swapped + * @param {number} indexB - The second index that points to the index in the array that will be swapped + * @returns {blocks.observable} - Returns the observable itself - return this; + * + * @example {javascript} + * var items = blocks.observable([4, 2, 3, 1]); + * + * // results in observable array with [1, 2, 3, 4] values + * items.swap(0, 3); + */ + swap: function (indexA, indexB) { + var array = this(); + var elements = this._elements; + var chunkManager = this._chunkManager; + var element; + + blocks.swap(array, indexA, indexB); + + for (var i = 0; i < elements.length; i++) { + element = elements[i].element; + if (indexA > indexB) { + chunkManager.insertAt(element, indexA, chunkManager.getAt(element, indexB)); + chunkManager.insertAt(element, indexB, chunkManager.getAt(element, indexA)); + } else { + chunkManager.insertAt(element, indexB, chunkManager.getAt(element, indexA)); + chunkManager.insertAt(element, indexA, chunkManager.getAt(element, indexB)); + } + } + + return this; + }, + + /** + * Moves an item from one location to another in the array. + * Note: Faster than removing the item and adding it at the location + * + * @memberof array + * @param {number} sourceIndex - The index pointing to the item that will be moved + * @param {number} targetIndex - The index where the item will be moved to + * @returns {blocks.observable} - Returns the observable itself - return this; + * + * @example {javascript} + * var items = blocks.observable([1, 4, 2, 3, 5]); + * + * // results in observable array with [1, 2, 3, 4, 5] values + * items.move(1, 4); + */ + move: function (sourceIndex, targetIndex) { + var array = this(); + var elements = this._elements; + var chunkManager = this._chunkManager; + var element; + + blocks.move(array, sourceIndex, targetIndex); + + if (targetIndex > sourceIndex) { + targetIndex++; + } + + for (var i = 0; i < elements.length; i++) { + element = elements[i].element; + chunkManager.insertAt(element, targetIndex, chunkManager.getAt(element, sourceIndex)); + } + + return this; + }, + + /** + * Removes an item from the observable array + * + * @memberof array + * @param {(Function|*)} value - the value that will be removed or a callback function + * which returns true or false to determine if the value should be removed + * @param {Function} [thisArg] - Optional this context for the callback + * @returns {blocks.observable} - Returns the observable itself - return this; + * + * @example {javascript} + * + */ + remove: function (value, thisArg) { + return this.removeAll(value, thisArg, true); + }, + + /** + * Removes an item at the specified index + * + * @memberof array + * @param {number} index - The index location of the item that will be removed + * @param {number} [count] - Optional parameter that if specified will remove + * the next items starting from the specified index + * @returns {blocks.observable} - Returns the observable itself - return this; + */ + removeAt: function (index, count) { + if (!blocks.isNumber(count)) { + count = 1; + } + this.splice(index, count); + + return this; + }, + + /** + * Removes all items from the observable array and optionally filter which items + * to be removed by providing a callback + * + * @memberof array + * @param {Function} [callback] - Optional callback function which filters which items + * to be removed. Returning a truthy value will remove the item and vice versa + * @param {*} [thisArg] - Optional this context for the callback function + * @returns {blocks.observable} - Returns the observable itself - return this; + */ + removeAll: function (callback, thisArg, removeOne) { + var array = this.__value__; + var chunkManager = this._chunkManager; + var items; + var i; + + if (arguments.length === 0) { + if (Events.has(this, 'removing') || Events.has(this, 'remove')) { + items = blocks.clone(array); + } + Events.trigger(this, 'removing', { + type: 'removing', + items: items, + index: 0 + }); + + chunkManager.remove(0, array.length); + + //this._indexes.splice(0, array.length); + this._indexes = []; + items = array.splice(0, array.length); + Events.trigger(this, 'remove', { + type: 'remove', + items: items, + index: 0 + }); + } else { + var isCallbackAFunction = blocks.isFunction(callback); + var value; + + for (i = 0; i < array.length; i++) { + value = array[i]; + if (value === callback || (isCallbackAFunction && callback.call(thisArg, value, i, array))) { + this.splice(i, 1); + i -= 1; + if (removeOne) { + break; + } + } + } + } + + this.update(); + + return this; + }, + + //#region Base + + /** + * The concat() method is used to join two or more arrays + * + * @memberof array + * @param {...Array} arrays - The arrays to be joined + * @returns {Array} - The joined array + */ + concat: function () { + var array = this(); + return array.concat.apply(array, blocks.toArray(arguments)); + }, + + // + /** + * The slice() method returns the selected elements in an array, as a new array object + * + * @memberof array + * @param {number} start An integer that specifies where to start the selection (The first element has an index of 0) + * @param {number} [end] An integer that specifies where to end the selection. If omitted, all elements from the start + * position and to the end of the array will be selected. Use negative numbers to select from the end of an array + * @returns {Array} A new array, containing the selected elements + */ + slice: function (start, end) { + if (arguments.length > 1) { + return this().slice(start, end); + } + return this().slice(start); + }, + + /** + * The join() method joins the elements of an array into a string, and returns the string + * + * @memberof array + * @param {string} [seperator=','] The separator to be used. If omitted, the elements are separated with a comma + * @returns {string} The array values, separated by the specified separator + */ + join: function (seperator) { + if (arguments.length > 0) { + return this().join(seperator); + } + return this().join(); + }, + + ///** + // * The indexOf() method returns the position of the first occurrence of a specified value in a string. + // * @param {*} item The item to search for. + // * @param {number} [index=0] Where to start the search. Negative values will start at the given position counting from the end, and search to the end. + // * @returns {number} The position of the specified item, otherwise -1 + // */ + //indexOf: function (item, index) { + // return blocks.indexOf(this(), item, index); + //}, + + + ///** + // * The lastIndexOf() method returns the position of the last occurrence of a specified value in a string. + // * @param {*} item The item to search for. + // * @param {number} [index=0] Where to start the search. Negative values will start at the given position counting from the end, and search to the beginning. + // * @returns {number} The position of the specified item, otherwise -1. + // */ + //lastIndexOf: function (item, index) { + // var array = this(); + // if (arguments.length > 1) { + // return blocks.lastIndexOf(array, item, index); + // } + // return blocks.lastIndexOf(array, item); + //}, + + //#endregion + + /** + * The pop() method removes the last element of a observable array, and returns that element + * + * @memberof array + * @returns {*} The removed array item + */ + pop: function () { + var that = this; + var array = that(); + + return that.splice(array.length - 1, 1)[0]; + }, + + /** + * The push() method adds new items to the end of the observable array, and returns the new length + * + * @memberof array + * @param {...*} values - The item(s) to add to the observable array + * @returns {number} The new length of the observable array + */ + push: function () { + this.addMany(arguments); + return this.__value__.length; + }, + + /** + * Reverses the order of the elements in the observable array + * + * @memberof array + * @returns {Array} The array after it has been reversed + */ + reverse: function () { + var array = this().reverse(); + var chunkManager = this._chunkManager; + + this._indexes.reverse(); + + chunkManager.each(function (domElement) { + for (var j = 1; j < array.length; j++) { + chunkManager.insertAt(domElement, 0, chunkManager.getAt(domElement, j)); + } + }); + + this.update(); + + return array; + }, + + /** + * Removes the first element of a observable array, and returns that element + * + * @memberof array + * @returns {*} The removed array item + */ + shift: function () { + return this.splice(0, 1)[0]; + //returns - The removed array item + }, + + /** + * Sorts the elements of an array + * + * @memberof array + * @param {Function} [sortfunction] - A function that defines the sort order + * @returns {Array} - The Array object, with the items sorted + */ + sort: function (sortfunction) { + var array = this.__value__; + var length = array.length; + var useSortFunction = arguments.length > 0; + var chunkManager = this._chunkManager; + var indexes = this._indexes; + var i = 0; + var j; + var item; + + for (; i < length; i++) { + var result = [array[i], i]; + + chunkManager.each(function (domElement) { + result.push(chunkManager.getAt(domElement, i)); + }); + //if (!useSortFunction) { // TODO: Test performance + // result.toString = function () { return this[0]; } + //} + array[i] = result; + } + + //if (useSortFunction) { // TODO: Test performance + // array.sort(function (a, b) { + // return sortfunction.call(this, a[0], b[0]) + // }); + //} + + // TODO: Test performance (Comment) + array.sort(function (a, b) { + a = a[0]; + b = b[0]; + if (useSortFunction) { + return sortfunction.call(this, a, b); + } + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }); + + if (indexes.length > 0) { + this._indexes = []; + } + + for (i = 0; i < length; i++) { + item = array[i]; + if (indexes.length > 0) { + this._indexes.push(indexes[item[1]]); + } + + j = 2; + chunkManager.each(function (domElement) { + chunkManager.insertAt(domElement, length, item[j]); + j++; + }); + array[i] = item[0]; + } + + this.update(); + + //chunkManager.dispose(); + + return array; + }, + + /** + * Adds and/or removes elements from the observable array + * + * @memberof array + * @param {number} index An integer that specifies at what position to add/remove items. + * Use negative values to specify the position from the end of the array. + * @param {number} howMany The number of items to be removed. If set to 0, no items will be removed. + * @param {...*} The new item(s) to be added to the array. + * @returns {Array} A new array containing the removed items, if any. + */ + splice: function (index, howMany) { + var array = this.__value__; + var chunkManager = this._chunkManager; + var returnValue = []; + var args = arguments; + var addItems; + + index = index < 0 ? array.length - index : index; + + if (howMany && index < array.length && index >= 0) { + howMany = Math.min(array.length - index, howMany); + returnValue = array.slice(index, index + howMany); + Events.trigger(this, 'removing', { + type: 'removing', + items: returnValue, + index: index + }); + + chunkManager.remove(index, howMany); + + returnValue = array.splice(index, howMany); + Events.trigger(this, 'remove', { + type: 'remove', + items: returnValue, + index: index + }); + } + + if (args.length > 2) { + addItems = blocks.toArray(args); + addItems.splice(0, 2); + Events.trigger(this, 'adding', { + type: 'adding', + index: index, + items: addItems + }); + + chunkManager.add(addItems, index); + + array.splice.apply(array, [index, 0].concat(addItems)); + Events.trigger(this, 'add', { + type: 'add', + index: index, + items: addItems + }); + } + + this.update(); + return returnValue; + }, + + /** + * The unshift() method adds new items to the beginning of an array, and returns the new length. + * + * @memberof array + * @this {blocks.observable} + * @param {...*} The new items that will be added to the beginning of the observable array. + * @returns {number} The new length of the observable array. + */ + unshift: function () { + this.addMany(arguments, 0); + return this.__value__.length; + } + } + } + }); + + + var ExtenderHelper = { + waiting: {}, + + initExpressionExtender: function (observable) { + var newObservable = observable.clone(); + + newObservable.view = blocks.observable([]); + newObservable.view._connections = {}; + newObservable.view._observed = []; + newObservable.view._updateObservable = blocks.bind(ExtenderHelper.updateObservable, newObservable); + newObservable._operations = observable._operations ? blocks.clone(observable._operations) : []; + newObservable._getter = blocks.bind(ExtenderHelper.getter, newObservable); + newObservable.view._initialized = false; + + newObservable.view.on('get', newObservable._getter); + + newObservable.on('add', function () { + if (newObservable.view._initialized) { + newObservable.view._connections = {}; + newObservable.view.reset(); + ExtenderHelper.executeOperations(newObservable); + } + }); + + newObservable.on('remove', function () { + if (newObservable.view._initialized) { + newObservable.view._connections = {}; + newObservable.view.reset(); + ExtenderHelper.executeOperations(newObservable); + } + }); + + return newObservable; + }, + + getter: function () { + Events.off(this.view, 'get', this._getter); + this._getter = undefined; + this.view._initialized = true; + ExtenderHelper.executeOperationsPure(this); + }, + + updateObservable: function () { + ExtenderHelper.executeOperations(this); + }, + + executeOperationsPure: function (observable) { + var chunk = []; + var observed = observable.view._observed; + var updateObservable = observable.view._updateObservable; + + blocks.each(observed, function (observable) { + Events.off(observable, 'change', updateObservable); + }); + observed = observable.view._observed = []; + Observer.startObserving(); + + blocks.each(observable._operations, function (operation) { + if (operation.type == 'step') { + var view = observable.view; + observable.view = blocks.observable([]); + observable.view._connections = {}; + if (chunk.length) { + ExtenderHelper.executeOperationsChunk(observable, chunk); + } + operation.step.call(observable.__context__); + observable.view = view; + } else { + chunk.push(operation); + } + }); + + if (chunk.length) { + ExtenderHelper.executeOperationsChunk(observable, chunk); + } + + blocks.each(Observer.stopObserving(), function (observable) { + observed.push(observable); + observable.on('change', updateObservable); + }); + }, + + executeOperations: function (observable) { + var id = observable.__id__; + var waiting = ExtenderHelper.waiting; + + if (!waiting[id]) { + waiting[id] = true; + setTimeout(function () { + ExtenderHelper.executeOperationsPure(observable); + waiting[id] = false; + }, 0); + } + }, + + executeOperationsChunk: function (observable, operations) { + var ADD = 'add'; + var REMOVE = 'remove'; + var EXISTS = 'exists'; + var action = EXISTS; + + var collection = observable.__value__; + var view = observable.view; + var connections = view._connections; + var newConnections = {}; + var viewIndex = 0; + var update = view.update; + var skip = 0; + var take = collection.length; + view.update = blocks.noop; + + blocks.each(operations, function (operation) { + if (operation.type == 'skip') { + skip = operation.skip; + if (blocks.isFunction(skip)) { + skip = skip.call(observable.__context__); + } + skip = blocks.unwrap(skip); + } else if (operation.type == 'take') { + take = operation.take; + if (blocks.isFunction(take)) { + take = take.call(observable.__context__); + } + take = blocks.unwrap(take); + } else if (operation.type == 'sort') { + if (blocks.isString(operation.sort)) { + collection = blocks.clone(collection).sort(function (valueA, valueB) { + return valueA[operation.sort] - valueB[operation.sort]; + }); + } else if (blocks.isFunction(operation.sort)) { + collection = blocks.clone(collection).sort(operation.sort); + } else { + collection = blocks.clone(collection).sort(); + } + if (operations.length == 1) { + operations.push({ type: 'filter', filter: function () { return true; }}); + } + } + }); + + blocks.each(collection, function iterateCollection(value, index) { + if (take <= 0) { + while (view().length - viewIndex > 0) { + view.removeAt(view().length - 1); + } + return false; + } + blocks.each(operations, function executeExtender(operation) { + var filterCallback = operation.filter; + + action = undefined; + + if (filterCallback) { + if (filterCallback.call(observable.__context__, value, index, collection)) { + action = EXISTS; + + if (connections[index] === undefined) { + action = ADD; + } + } else { + action = undefined; + if (connections[index] !== undefined) { + action = REMOVE; + } + return false; + } + } else if (operation.type == 'skip') { + action = EXISTS; + skip -= 1; + if (skip >= 0) { + action = REMOVE; + return false; + } else if (skip < 0 && connections[index] === undefined) { + action = ADD; + } + } else if (operation.type == 'take') { + if (take <= 0) { + action = REMOVE; + return false; + } else { + take -= 1; + action = EXISTS; + + if (connections[index] === undefined) { + action = ADD; + } + } + } + }); + + switch (action) { + case ADD: + newConnections[index] = viewIndex; + view.splice(viewIndex, 0, value); + viewIndex++; + break; + case REMOVE: + view.removeAt(viewIndex); + break; + case EXISTS: + newConnections[index] = viewIndex; + viewIndex++; + break; + } + }); + + view._connections = newConnections; + view.update = update; + view.update(); + } + }; + + + + /** + * @memberof blocks.observable + * @class extenders + */ + + /** + * Extends the observable by adding a .view property which is filtered + * based on the provided options + * + * @memberof extenders + * @param {(Function|Object|String)} options - provide a callback function + * which returns true or false, you could also provide an observable + * @returns {blocks.observable} - Returns a new observable + * containing a .view property with the filtered data + */ + blocks.observable.filter = function (options) { + var observable = ExtenderHelper.initExpressionExtender(this); + var callback = options; + + if (!blocks.isFunction(callback) || blocks.isObservable(callback)) { + callback = function (value) { + var filter = blocks.unwrap(options); + var filterString = String(filter).toLowerCase(); + value = String(blocks.unwrap(value)).toLowerCase(); + + return !filter || value.indexOf(filterString) != -1; + }; + } + + observable._operations.push({ + type: 'filter', + filter: callback + }); + + return observable; + }; + + blocks.observable.step = function (options) { + var observable = ExtenderHelper.initExpressionExtender(this); + + observable._operations.push({ + type: 'step', + step: options + }); + + return observable; + }; + + /** + * Extends the observable by adding a .view property in which the first n + * items are skipped + * + * @memberof extenders + * @param {(number|blocks.observable)} value - The number of items to be skipped + * @returns {blocks.observable} - Returns a new observable + * containing a .view property with the manipulated data + */ + blocks.observable.skip = function (value) { + var observable = ExtenderHelper.initExpressionExtender(this); + + observable._operations.push({ + type: 'skip', + skip: value + }); + + return observable; + }; + + /** + * Extends the observable by adding a .view property in which there is + * always maximum n items + * + * @memberof extenders + * @param {(number|blocks.observable))} value - The max number of items to be in the collection + * @returns {blocks.observable} - Returns a new observable + * containing a .view property with the manipulated data + */ + blocks.observable.take = function (value) { + var observable = ExtenderHelper.initExpressionExtender(this); + + observable._operations.push({ + type: 'take', + take: value + }); + + return observable; + }; + + /** + * Extends the observable by adding a .view property which is sorted + * based on the provided options + * + * @memberof extenders + * @param {(Function|string)} options - provide a callback sort function or field name to be sorted + * @returns {blocks.observable} - Returns a new observable + * containing a .view property with the sorted data + */ + blocks.observable.sort = function (options) { + var observable = ExtenderHelper.initExpressionExtender(this); + + observable._operations.push({ + type: 'sort', + sort: options + }); + + return observable; + }; + + + /** + * Performs a query operation on the DOM. Executes all data-query attributes + * and renders the html result to the specified HTMLElement if not specified + * uses document.body by default. + * + * @memberof blocks + * @param {*} model - The model that will be used to query the DOM. + * @param {HTMLElement} [element=document.body] - Optional element on which to execute the query. + * + * @example {html} + * + *

Hey, {{message}}

+ * + * + *

Hey, Hello World!

+ */ + blocks.query = function query(model, element) { + blocks.domReady(function () { + blocks.$unwrap(element, function (element) { + if (!blocks.isElement(element)) { + element = document.body; + } + + var domQuery = new DomQuery(); + var rootElement = createVirtual(element)[0]; + var serverData = window.__blocksServerData__; + + domQuery.pushContext(model); + domQuery._serverData = serverData; + + if (serverData) { + rootElement.render(domQuery); + } else { + rootElement.sync(domQuery); + } + domQuery.createElementObservableDependencies([element]); + }); + }); + }; + + blocks.executeQuery = function executeQuery(element, queryName /*, ...args */) { + var methodName = VirtualElement.Is(element) ? 'preprocess' : 'update'; + var args = Array.prototype.slice.call(arguments, 2); + var query = blocks.queries[queryName]; + if (query.passDomQuery) { + args.unshift(blocks.domQuery(element)); + } + query[methodName].apply(element, args); + }; + + /** + * Gets the context for a particular element. Searches all parents until it finds the context. + * + * @memberof blocks + * @param {(HTMLElement|blocks.VirtualElement)} element - The element from which to search for a context + * @returns {Object} - The context object containing all context properties for the specified element + * + * @example {html} + * + * + */ + blocks.context = function context(element, isRecursive) { + element = blocks.$unwrap(element); + + if (element) { + var elementData = ElementsData.data(element); + if (elementData) { + if (isRecursive && elementData.childrenContext) { + return elementData.childrenContext; + } + if (elementData.context) { + return elementData.context; + } + } + + return blocks.context(VirtualElement.Is(element) ? element._parent : element.parentNode, true); + } + return null; + }; + + /** + * Gets the associated dataItem for a particlar element. Searches all parents until it finds the context + * + * @memberof blocks + * @param {(HTMLElement|blocks.VirtualElement)} element - The element from which to search for a dataItem + * @returns {*} + * + * @example {html} + * + * + */ + blocks.dataItem = function dataItem(element) { + var context = blocks.context(element); + return context ? context.$this : null; + }; + + /** + * Determines if particular value is an blocks.observable + * + * @memberof blocks + * @param {*} value - The value to check if the value is observable + * @returns {boolean} - Returns if the value is observable + * + * @example {javascript} + * blocks.isObservable(blocks.observable(3)); + * // -> true + * + * blocks.isObservable(3); + * // -> false + */ + blocks.isObservable = function isObservable(value) { + return !!value && value.__identity__ === OBSERVABLE; + }; + + /** + * Gets the raw value of an observable or returns the value if the specified object is not an observable + * + * @memberof blocks + * @param {*} value - The value that could be any object observable or not + * @returns {*} - Returns the unwrapped value + * + * @example {javascript} + * blocks.unwrapObservable(blocks.observable(304)); + * // -> 304 + * + * blocks.unwrapObservable(305); + * // -> 305 + */ + blocks.unwrapObservable = function unwrapObservable(value) { + if (value && value.__identity__ === OBSERVABLE) { + return value(); + } + return value; + }; + + blocks.domQuery = function domQuery(element) { + element = blocks.$unwrap(element); + if (element) { + var data = ElementsData.data(element, 'domQuery'); + if (data) { + return data; + } + return blocks.domQuery(VirtualElement.Is(element) ? element._parent : element.parentNode); + } + return null; + }; + + + + blocks.extend(blocks.queries, { + /** + * Associates the element with the particular view and creates a $view context property. + * The View will be automatically hidden and shown if the view have routing. The visibility + * of the View could be also controled using the isActive observable property + * + * @memberof blocks.queries + * @param {View} view - The view to associate with the current element + * + * @example {html} + * + *
+ * + * + *
+ * + * @example {javascript} + * var App = blocks.Application(); + * + * App.View('Profiles', { + * users: [{ username: 'John' }, { username: 'Doe' }], + * + * selectUser: function (e) { + * // ...stuff... + * } + * }); + */ + view: { + passDomQuery: true, + + preprocess: function (domQuery, view) { + if (!view.isActive()) { + this.css('display', 'none'); + } else { + //view._tryInitialize(view.isActive()); + this.css('display', ''); + if (view._html) { + blocks.queries.template.preprocess.call(this, domQuery, view._html, view); + } + // Quotes are used because of IE8 and below. It failes with 'Expected idenfitier' + //queries['with'].preprocess.call(this, domQuery, view, '$view'); + //queries.define.preprocess.call(this, domQuery, view._name, view); + } + + queries['with'].preprocess.call(this, domQuery, view, '$view'); + }, + + update: function (domQuery, view) { + if (view.isActive()) { + if (view._html) { + // Quotes are used because of IE8 and below. It failes with 'Expected idenfitier' + queries['with'].preprocess.call(this, domQuery, view, '$view'); + + this.innerHTML = view._html; + view._children = view._html = undefined; + blocks.each(createVirtual(this.childNodes[0]), function (element) { + if (VirtualElement.Is(element)) { + element.sync(domQuery); + } else if (element && element.isExpression && element.element) { + element.element.nodeValue = Expression.GetValue(domQuery._context, null, element); + } + }); + domQuery.createElementObservableDependencies(this.childNodes); + } + animation.show(this); + } else { + animation.hide(this); + } + } + }, + + /** + * Navigates to a particular view by specifying the target view or route and optional parameters + * + * @memberof blocks.queries + * @param {(View|String)} viewOrRoute - the view or route to which to navigate to + * @param {Object} [params] - parameters needed for the current route + * + * @example {html} + * + * Contact Us + * + * + * T-Shirts + * + * + * T-Shirts + */ + navigateTo: { + update: function (viewOrRoute, params) { + function navigate(e) { + e = e || window.event; + e.preventDefault(); + e.returnValue = false; + + if (blocks.isString(viewOrRoute)) { + window.location.href = viewOrRoute; + } else { + viewOrRoute.navigateTo(viewOrRoute, params); + } + } + + addListener(this, 'click', navigate); + } + }, + + trigger: { + + } + }); + + + var validators = { + required: { + priority: 9, + validate: function (value, options) { + if (value !== options.defaultValue && + value !== '' && + value !== false && + value !== undefined && + value !== null) { + return true; + } + } + }, + + minlength: { + priority: 19, + validate: function (value, options, option) { + if (value === undefined || value === null) { + return false; + } + return value.length >= parseInt(option, 10); + } + }, + + maxlength: { + priority: 29, + validate: function (value, options, option) { + if (value === undefined || value === null) { + return true; + } + return value.length <= parseInt(option, 10); + } + }, + + min: { + priority: 39, + validate: function (value, options, option) { + if (value === undefined || value === null) { + return false; + } + return value >= option; + } + }, + + max: { + priority: 49, + validate: function (value, options, option) { + if (value === undefined || value === null) { + return false; + } + return value <= option; + } + }, + + email: { + priority: 59, + validate: function (value) { + return /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(value); + } + }, + + url: { + priority: 69, + validate: function (value) { + return /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/.test(value); + } + }, + + date: { + priority: 79, + validate: function (value) { + if (!value) { + return false; + } + return !/Invalid|NaN/.test(new Date(value.toString()).toString()); + } + }, + + creditcard: { + priority: 89, + validate: function (value) { + if (blocks.isString(value) && value.length === 0) { + return false; + } + if (blocks.isNumber(value)) { + value = value.toString(); + } + // accept only spaces, digits and dashes + if (/[^0-9 \-]+/.test(value)) { + return false; + } + var nCheck = 0, + nDigit = 0, + bEven = false; + + value = value.replace(/\D/g, ''); + + for (var n = value.length - 1; n >= 0; n--) { + var cDigit = value.charAt(n); + nDigit = parseInt(cDigit, 10); + if (bEven) { + if ((nDigit *= 2) > 9) { + nDigit -= 9; + } + } + nCheck += nDigit; + bEven = !bEven; + } + + return (nCheck % 10) === 0; + } + }, + + regexp: { + priority: 99, + validate: function (value, options, option) { + if (!blocks.isRegExp(option)) { + return false; + } + if (value === undefined || value === null) { + return false; + } + return option.test(value); + } + }, + + number: { + priority: 109, + validate: function (value) { + if (blocks.isNumber(value)) { + return true; + } + if (blocks.isString(value) && value.length === 0) { + return false; + } + return /^(-?|\+?)(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value); + } + }, + + digits: { + priority: 119, + validate: function (value) { + return /^\d+$/.test(value); + } + }, + + letters: { + priority: 129, + validate: function (value) { + if (!value) { + return false; + } + return /^[a-zA-Z]+$/.test(value); + } + }, + + equals: { + priority: 139, + validate: function (value, options, option) { + return blocks.equals(value, blocks.unwrap(option)); + } + } + }; + + // TODO: asyncValidate + blocks.observable.validation = function (options) { + var _this = this; + var maxErrors = options.maxErrors; + var errorMessages = this.errorMessages = blocks.observable([]); + var validatorsArray = this._validators = []; + var key; + var option; + + this.errorMessage = blocks.observable(''); + + for (key in options) { + option = options[key]; + if (validators[key]) { + validatorsArray.push({ + option: option, + validate: validators[key].validate, + priority: validators[key].priority + }); + } else if (key == 'validate' || key == 'asyncValidate') { + validatorsArray.push({ + option: '', + validate: option.validate ? option.validate : option, + priority: option.priority || Number.POSITIVE_INFINITY, + isAsync: key == 'asyncValidate' + }); + } + } + + validatorsArray.sort(function (a, b) { + return a.priority > b.priority ? 1 : -1; + }); + + this.valid = blocks.observable(true); + + this.validate = function () { + var value = _this.__value__; + var isValid = true; + var errorsCount = 0; + var i = 0; + var validationOptions; + var validator; + var message; + + errorMessages.removeAll(); + for (; i < validatorsArray.length; i++) { + if (errorsCount >= maxErrors) { + break; + } + validator = validatorsArray[i]; + if (validator.isAsync) { + validator.validate(value, function (result) { + validationComplete(_this, options, !!result); + }); + return true; + } else { + validationOptions = validator.option; + option = validator.option; + if (blocks.isPlainObject(validationOptions)) { + option = validationOptions.value; + } + if (blocks.isFunction(option)) { + option = option.call(_this.__context__); + } + message = validator.validate(value, options, option); + if (blocks.isString(message)) { + message = [message]; + } + if (blocks.isArray(message) || !message) { + errorMessages.addMany( + blocks.isArray(message) ? message : + validationOptions && validationOptions.message ? [validationOptions.message] : + option && blocks.isString(option) ? [option] : + []); + isValid = false; + errorsCount++; + } + } + } + + validationComplete(this, options, isValid); + this.valid(isValid); + Events.trigger(this, 'validate'); + return isValid; + }; + + if (options.validateOnChange) { + this.on('change', function () { + this.validate(); + }); + } + if (options.validateInitially) { + this.validate(); + } + }; + + function validationComplete(observable, options, isValid) { + var errorMessage = observable.errorMessage; + var errorMessages = observable.errorMessages; + + if (isValid) { + errorMessage(''); + } else { + errorMessage(options.errorMessage || errorMessages()[0] || ''); + } + + observable.valid(isValid); + } + + + function escapeRegEx(string) { + return string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + } + + blocks.route = function (route) { + return Route(route); + }; + + function Route(route) { + if (Route.Is(route)) { + return route; + } + if (!Route.Is(this)) { + return new Route(route); + } + + this._routeString = route; + this._wildcard = {}; + this._optional = {}; + this._validate = {}; + this._transform = {}; + } + + Route.Is = function (route) { + return Route.prototype.isPrototypeOf(route); + }; + + Route.Has = function (route) { + return route._routeString != null; + }; + + Route.Combine = function (routeA, routeB) { + if (!Route.Has(routeB)) { + return routeA; + } + if (!Route.Has(routeA)) { + return routeB; + } + + var route = Route(routeA + routeB); + blocks.extend(route._wildcard, routeA._wildcard, routeB._wildcard); + blocks.extend(route._optional, routeA._optional, routeB._optional); + blocks.extend(route._validate, routeA._validate, routeB._validate); + return route; + }; + + Route.prototype = { + wildcard: function () { + var wildcard = this._wildcard; + var wildcards = blocks.flatten(blocks.toArray(arguments)); + blocks.each(wildcards, function (value) { + wildcard[value] = true; + }); + + return this; + }, + + optional: function (nameOrObject, value) { + this._addMetadata('optional', nameOrObject, value); + + return this; + }, + + validate: function (nameOrObject, value) { + this._addMetadata('validate', nameOrObject, value); + + return this; + }, + + transform: function (nameOrObject, value) { + this._addMetadata('_transform', nameOrObject, value); + + return this; + }, + + toString: function () { + return this._routeString ? this._routeString.toString() : ''; + }, + + trailingSlash: function () { + return this; + }, + + _transfromParam: function (paramName, value) { + var transform = this._transform[paramName]; + if (value === '' && blocks.has(this._optional, paramName)) { + value = this._optional[paramName]; + } + if (transform) { + return transform(value); + } + return value; + }, + + _validateParam: function (paramName, value) { + var validator = this._validate[paramName]; + if (validator) { + return validator(value); + } + return true; + }, + + _addMetadata: function (type, nameOrObject, value) { + var metadata = this['_' + type]; + + if (blocks.isPlainObject(nameOrObject)) { + blocks.each(nameOrObject, function (val, key) { + metadata[key] = val; + }); + } else if (blocks.isString(nameOrObject)) { + metadata[nameOrObject] = value; + } + } + }; + + function Router() { + this._currentRoute = {}; + this._routes = {}; + } + + blocks.core.Router = Router; + + Router.GenerateRoute = function (routeString, params) { + var router = new Router(); + var routeId = router.registerRoute(routeString); + var route = router.routeTo(routeId, params); + + if (routeString.indexOf('/') === 0 && route.indexOf('/') !== 0) { + return '/' + route; + } + + return route; + }; + + Router.prototype = { + registerRoute: function (route, parentRoute) { + route = Route(route); + parentRoute = parentRoute ? Route(this._routes[Route(parentRoute).toString()].route) : Route(undefined); + + var finalRoute = Route.Combine(parentRoute, route); + var routeId = finalRoute._routeString = finalRoute._routeString.replace(/^\//, ''); + var routeData = this._generateRouteStringData(routeId); + + this._routes[routeId] = { + route: finalRoute, + data: routeData, + regExCollection: this._generateRouteRegEx(finalRoute, routeData), + parent: Route.Has(parentRoute) ? this._routes[parentRoute.toString()] : undefined + }; + + return routeId; + }, + + routeTo: function (routeId, params) { + var routeMetadata = this._routes[routeId]; + var route = routeMetadata.route; + var result = ''; + var param; + + params = params || {}; + + blocks.each(routeMetadata.data, function (split) { + param = split.param; + if (param) { + if (route._validateParam(params[param])) { + result += blocks.has(params, param) ? params[param] : route._optional[param]; + } + } else { + result += split.string; + } + }); + + return result; + }, + + routeFrom: function (url) { + var getUrlParams = this._getUrlParams; + var result = []; + var matches; + + url = decodeURI(url); + + blocks.each(this._routes, function (routeMetadata) { + blocks.each(routeMetadata.regExCollection, function (regEx) { + if (regEx.regEx.test(url)) { + matches = regEx.regEx.exec(url); + while (routeMetadata) { + result.unshift({ + id: routeMetadata.route._routeString, + params: getUrlParams(routeMetadata, regEx.params, matches) + }); + routeMetadata = routeMetadata.parent; + } + return false; + } + }); + }); + + return result.length ? result : null; + }, + + _getUrlParams: function (routeMetadata, params, matches) { + var route = routeMetadata.route; + var result = {}; + var value; + var param; + + blocks.each(params, function (param, index) { + value = matches[index + 1]; + if (route._validateParam(param, value)) { + result[param] = route._transfromParam(param, value); + } + }); + + blocks.each(routeMetadata.data, function (split) { + param = split.param; + if (param && !result[param] && + blocks.has(route._optional, param) && route._optional[param] !== undefined) { + + result[param] = route._optional[param]; + } + }); + + return result; + }, + + _generateRouteRegEx: function (route, routeData) { + var result = []; + var sliceLastFromRegExString = this._sliceLastFromRegExString; + var combinations = this._getOptionalParametersCombinations(route, routeData); + var allOptionalBetweenForwardSlash; + var containsParameter; + var regExString; + var params; + var param; + + blocks.each(combinations, function (skipParameters) { + regExString = '^'; + params = []; + + blocks.each(routeData, function (split) { + param = split.param; + if (param) { + containsParameter = true; + if (!blocks.has(route._optional, param) || !skipParameters[param]) { + allOptionalBetweenForwardSlash = false; + } + if (skipParameters[param]) { + return; + } else { + params.push(param); + } + if (route._wildcard[param]) { + regExString += blocks.has(route._optional, param) ? '(.*?)' : '(.+?)'; + } else { + regExString += blocks.has(route._optional, param) ? '([^\/]*?)' : '([^\/]+?)'; + } + } else { + if (split.string == '/') { + if (containsParameter && allOptionalBetweenForwardSlash) { + regExString = sliceLastFromRegExString(regExString); + } + containsParameter = false; + allOptionalBetweenForwardSlash = true; + } + regExString += escapeRegEx(split.string); + } + }); + + if (containsParameter && allOptionalBetweenForwardSlash) { + regExString = sliceLastFromRegExString(regExString); + } + + result.push({ + regEx: new RegExp(regExString + '$', 'i'), + params: params + }); + }); + + return result; + }, + + _sliceLastFromRegExString: function (regExString) { + var index; + + for (var i = 0; i < regExString.length; i++) { + index = regExString.length - i - 1; + if (regExString.charAt(index) == '/' && regExString.charAt(index + 1) != ']') { + break; + } + } + + return regExString.substring(0, index - 1); + }, + + _getOptionalParametersCombinations: function (route, routeData) { + var optionalParameters = this._getOptionalParameters(route, routeData); + var iterations = Math.pow(2, optionalParameters.length); + var length = optionalParameters.length; + var combinations = [{}]; + var current; + var i; + var j; + + for (i = 0; i < iterations ; i++) { + current = {}; + current.__lowestIndex__ = length; + current.__length__ = 0; + for (j = 0; j < length; j++) { + /* jshint bitwise: false */ + if ((i & Math.pow(2, j))) { + if (j < current.__lowestIndex__) { + current.__lowestIndex__ = j; + } + current[optionalParameters[j]] = true; + current.__length__ += 1; + } + } + if (current.__length__) { + combinations.push(current); + } + } + + combinations.sort(function (x, y) { + var result = x.__length__ - y.__length__; + + if (!result) { + return y.__lowestIndex__ - x.__lowestIndex__; + } + + return result; + }); + + return combinations; + }, + + _getOptionalParameters: function (route, routeData) { + var optionalParameters = []; + + blocks.each(routeData, function (split) { + if (blocks.has(route._optional, split.param)) { + optionalParameters.push(split.param); + } + }); + + return optionalParameters; + }, + + _generateRouteStringData: function (routeString) { + var pushStringData = this._pushStringData; + var data = []; + var lastIndex = 0; + + routeString.replace(/{{[^}]+}}/g, function (match, startIndex) { + pushStringData(data, routeString.substring(lastIndex, startIndex)); + lastIndex = startIndex + match.length; + data.push({ + param: match.substring(2, match.length - 2) + }); + }); + + if (lastIndex != routeString.length) { + pushStringData(data, routeString.substring(lastIndex)); + } + + return data; + }, + + _pushStringData: function (data, string) { + var splits = string.split('/'); + blocks.each(splits, function (split, index) { + if (split) { + data.push({ + string: split + }); + } + if (index != splits.length - 1) { + data.push({ + string: '/' + }); + } + }); + } + }; + + var uniqueId = (function () { + var timeStamp = Date.now(); + return function () { + return 'blocks_' + blocks.version + '_' + timeStamp++; + }; + })(); + + function Request(options) { + this.options = blocks.extend({}, Request.Defaults, options); + this.execute(); + } + + Request.Execute = function (options) { + return new Request(options); + }; + + Request.Defaults = { + type: 'GET', + url: '', + processData: true, + async: true, + contentType: 'application/x-www-form-urlencoded; charset=UTF-8', + jsonp: 'callback', + jsonpCallback: function () { + return uniqueId(); + } + + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + }; + + Request.Accepts = { + '*': '*/'.concat('*'), + text: 'text/plain', + html: 'text/html', + xml: 'application/xml, text/xml', + json: 'application/json, text/javascript' + }; + + Request.Meta = { + statusFix: { + // file protocol always yields status code 0, assume 200 + 0: 200, + // Support: IE9 + // IE sometimes returns 1223 instead of 204 + 1223: 204 + } + }; + + Request.prototype = { + execute: function () { + var options = this.options; + var serverData = window.__blocksServerData__; + + if (options.type == 'GET' && options.data) { + this.appendDataToUrl(options.data); + } + + if (serverData && serverData.requests && serverData.requests[options.url]) { + this.callSuccess(serverData.requests[options.url]); + } else { + try { + if (options.dataType == 'jsonp') { + this.scriptRequest(); + } else { + this.xhrRequest(); + } + } catch (e) { + + } + } + }, + + xhrRequest: function () { + var options = this.options; + var xhr = this.createXHR(); + + xhr.onabort = blocks.bind(this.xhrError, this); + xhr.ontimeout = blocks.bind(this.xhrError, this); + xhr.onload = blocks.bind(this.xhrLoad, this); + xhr.onerror = blocks.bind(this.xhrError, this); + xhr.open(options.type.toUpperCase(), options.url, options.async, options.username, options.password); + xhr.setRequestHeader('Content-Type', options.contentType); + xhr.setRequestHeader('Accept', Request.Accepts[options.dataType || '*']); + xhr.send(options.data || null); + }, + + createXHR: function () { + var Type = XMLHttpRequest || window.ActiveXObject; + try { + return new Type('Microsoft.XMLHTTP'); + } catch (e) { + + } + }, + + xhrLoad: function (e) { + var request = e.target; + var status = Request.Meta.statusFix[request.status] || request.status; + var isSuccess = status >= 200 && status < 300 || status === 304; + if (isSuccess) { + this.callSuccess(request.responseText); + } else { + this.callError(request.statusText); + } + }, + + xhrError: function () { + this.callError(); + }, + + scriptRequest: function () { + var that = this; + var options = this.options; + var script = document.createElement('script'); + var jsonpCallback = {}; + var callbackName = blocks.isFunction(options.jsonpCallback) ? options.jsonpCallback() : options.jsonpCallback; + + jsonpCallback[options.jsonp] = callbackName; + this.appendDataToUrl(jsonpCallback); + window[callbackName] = function (result) { + window[callbackName] = null; + that.scriptLoad(result); + }; + + script.onerror = this.scriptError; + script.async = options.async; + script.src = options.url; + document.head.appendChild(script); + }, + + scriptLoad: function (data) { + this.callSuccess(data); + }, + + scriptError: function () { + this.callError(); + }, + + appendDataToUrl: function (data) { + var that = this; + var options = this.options; + var hasParameter = /\?/.test(options.url); + + if (blocks.isPlainObject(data)) { + blocks.each(data, function (value, key) { + options.url += that.append(hasParameter, key, value.toString()); + }); + } else if (blocks.isArray(data)) { + blocks.each(data, function (index, value) { + that.appendDataToUrl(value); + }); + } else { + options.url += that.append(hasParameter, data.toString(), ''); + } + }, + + append: function (hasParameter, key, value) { + var result = hasParameter ? '&' : '?'; + result += key; + if (value) { + result += '=' + value; + } + return result; + }, + + callSuccess: function (data) { + var success = this.options.success; + var textStatus = 'success'; + if (success) { + success(data, textStatus, null); + } + this.callComplete(textStatus); + }, + + callError: function (errorThrown) { + var error = this.options.error; + var textStatus = 'error'; + if (error) { + error(null, textStatus, errorThrown); + } + this.callComplete(textStatus); + }, + + callComplete: function (textStatus) { + var complete = this.options.complete; + if (complete) { + complete(null, textStatus); + } + } + }; + + + function ajax(options) { + if (window) { + var jQuery = window.jQuery || window.$; + if (jQuery && jQuery.ajax) { + jQuery.ajax(options); + } else { + Request.Execute(options); + } + } + } + + /* global JSON */ + + var CREATE = 'create'; + var UPDATE = 'update'; + var DESTROY = 'destroy'; + var GET = 'GET'; + var CONTENT_TYPE = 'application/json; charset=utf-8'; + //var JSONP = 'jsonp'; + var EVENTS = [ + 'change', + 'sync', + 'error', + 'requestStart', + 'requestEnd' + ]; + + function DataSource(options) { + options = options || {}; + var data = options.data; + var baseUrl = options.baseUrl; + + // set options.data to undefined and return the extended options object using || + options = this.options = (options.data = undefined) || blocks.extend(true, {}, this.options, options); + + if (baseUrl) { + options.read.url = baseUrl + options.read.url; + options.create.url = baseUrl + options.create.url; + options.destroy.url = baseUrl + options.destroy.url; + options.update.url = baseUrl + options.update.url; + } + + this.data = blocks + .observable(blocks.unwrap(data) || []) + .extend() + .on('add remove', blocks.bind(this._onArrayChange, this)); + this.hasChanges = blocks.observable(false); + + this._aggregates = null; + this._changes = []; + this._changesMeta = {}; + + this._subscribeToEvents(); + } + + blocks.DataSource = DataSource; + + DataSource.ArrayMode = 1; + DataSource.ObjectMode = 2; + + DataSource.prototype = { + options: { + baseUrl: '', + idAttr: '', + mode: DataSource.ArrayMode, + + read: { + url: '', + type: GET, + contentType: CONTENT_TYPE + }, + + update: { + url: '', + type: 'POST', + contentType: CONTENT_TYPE + }, + + create: { + url: '', + type: 'POST', + contentType: CONTENT_TYPE + }, + + destroy: { + url: '', + type: 'POST', + contentType: CONTENT_TYPE + } + }, + + read: function (options, callback) { + var _this = this; + + callback = arguments[arguments.length - 1]; + if (blocks.isFunction(options)) { + options = {}; + } + options = options || {}; + + _this._ajax('read', options, function (data) { + if (blocks.isString(data)) { + data = JSON.parse(data); + } + + if (_this.options.mode == DataSource.ArrayMode) { + if (!blocks.isArray(data)) { + if (blocks.isArray(data.value)) { + data = data.value; + } else if (blocks.isObject(data)) { + blocks.each(data, function (value) { + if (blocks.isArray(value)) { + data = value; + return false; + } + }); + } + } + } + + if (!blocks.isArray(data)) { + data = [data]; + } + + if (!options || options.__updateData__ !== false) { + _this._updateData(data); + } + if (callback && blocks.isFunction(callback)) { + callback(data); + } + }); + return _this; + }, + + // should accept dataItem only + // should accept id + object with the new data + update: function () { + if (arguments.length === 0) { + return; + } + var items; + if (arguments.length > 1 && blocks.type(arguments[0]) != blocks.type(arguments[1])) { + items = [arguments[1]]; + items[0][this.options.idAttr] = arguments[0]; + } else { + items = blocks.flatten(arguments); + } + if (items.length > 0) { + this._changes.push({ + type: UPDATE, + items: items + }); + this._onChangePush(); + } + }, + + hasChanges: function () { + return this._changes.length > 0; + }, + + clearChanges: function () { + this._changes.splice(0, this._changes.length); + this._changesMeta = {}; + this.hasChanges(false); + return this; + }, + + sync: function (callback) { + var _this = this; + var changes = this._changes; + var changesLeft = changes.length; + var data; + + blocks.each(changes, function (change) { + blocks.each(change.items, function (item) { + data = item; + if (item.__id__) { + delete item.__id__; + } + _this._ajax(change.type, { + data: data + }, function () { + changesLeft--; + if (!changesLeft) { + if (blocks.isFunction(callback)) { + callback(); + } + _this._trigger('sync'); + } + }); + }); + }); + + return this.clearChanges(); + }, + + _ajax: function (optionsName, options, callback) { + var _this = this; + var type; + + options = blocks.extend({}, this.options[optionsName], options); + type = options.type.toUpperCase(); + options.url = Router.GenerateRoute(options.url, options.data); + this._trigger('requestStart', { + + }); + ajax({ + type: options.type, + url: options.url, + data: type == GET ? null : JSON.stringify(options.data), + contentType: options.contentType, // 'application/json; charset=utf-8', + dataType: options.dataType, + jsonp: options.jsonp, + success: function (data, statusMessage, status) { + _this._trigger('requestEnd', {}); + if (data) { + callback(data, statusMessage, status); + } + }, + error: function (/*message, statusObject, status*/) { + _this._trigger('requestEnd', {}); + _this._trigger('error'); + } + }); + }, + + _updateData: function (data) { + this.data.removeAll(); + this.data.addMany(data); + + this.clearChanges(); + this._trigger('change'); + }, + + _onArrayChange: function (args) { + var type = args.type; + if (type == 'remove') { + this._remove(args.items); + } else if (type == 'removeAt') { + this._remove(this.data.slice(args.index, args.index + args.count)); + } else if (type == 'add') { + this._add(args.items); + } + }, + + _onChangePush: function () { + var metadata = this._changesMeta; + var changes = this._changes; + var change = changes[changes.length - 1]; + var idAttr = this.options.idAttr; + var type = change.type; + var metaItem; + + blocks.each(change.items, function (item) { + switch (type) { + case CREATE: + item.__id__ = uniqueId(); + metadata[item.__id__] = item; + break; + case UPDATE: + metaItem = metadata[item[idAttr]]; + if (metaItem) { + changes.splice(metaItem.index, 1); + metaItem.item = item; + metaItem.index = changes.length - 1; + } + metadata[item[idAttr]] = { + index: changes.length - 1, + item: item + }; + break; + case DESTROY: + metaItem = metadata[item ? item.__id__ : undefined]; + if (metaItem) { + changes.splice(metaItem.index, 1); + changes.pop(); + metadata[item.__id__] = undefined; + } + break; + } + }); + + if (changes.length > 0 && this.options.autoSync) { + this.sync(); + } else { + this.hasChanges(changes.length > 0); + } + }, + + _add: function (items) { + this._changes.push({ + type: CREATE, + items: items + }); + this._onChangePush(); + }, + + _remove: function (items) { + this._changes.push({ + type: DESTROY, + items: items + }); + this._onChangePush(); + }, + + _subscribeToEvents: function () { + var _this = this; + var options = this.options; + + blocks.each(EVENTS, function (value) { + if (options[value]) { + _this.on(value, options[value]); + } + }); + } + }; + + Events.register(DataSource.prototype, [ + 'on', + '_trigger', + + + // TODO: Should remove these + 'change', + 'error', + 'requestStart', + 'requestEnd' + ]); + + blocks.core.applyExpressions('array', blocks.DataSource.prototype, blocks.toObject([/*'remove', 'removeAt', 'removeAll', 'add',*/ 'size', 'at', 'isEmpty', 'each'])); + + + + function Property(options) { + this._options = options || {}; + } + + Property.Is = function (value) { + return Property.prototype.isPrototypeOf(value); + }; + + Property.Inflate = function (object) { + var properties = {}; + var key; + var value; + + for (key in object) { + value = object[key]; + if (Property.Is(value)) { + value = value._options; + value.propertyName = key; + properties[value.field || key] = value; + } + } + + return properties; + }; + + Property.Create = function (options, thisArg, value) { + var observable; + + if (arguments.length < 3) { + value = options.value || options.defaultValue; + } + thisArg = options.thisArg ? options.thisArg : thisArg; + + observable = blocks + .observable(value, thisArg) + .extend('validation', options) + .on('changing', options.changing, thisArg) + .on('change', options.change, thisArg); + + blocks.each(options.extenders, function (extendee) { + observable = observable.extend.apply(observable, extendee); + }); + + return observable; + }; + + Property.prototype.extend = function () { + var options = this._options; + options.extenders = options.extenders || []; + options.extenders.push(blocks.toArray(arguments)); + + return this; + }; + + + + /** + * @namespace Model + */ + function Model(application, prototype, dataItem, collection) { + var _this = this; + this._application = application; + this._collection = collection; + this._initialDataItem = blocks.clone(dataItem, true); + + blocks.each(Model.prototype, function (value, key) { + if (blocks.isFunction(value) && key.indexOf('_') !== 0) { + _this[key] = blocks.bind(value, _this); + } + }); + clonePrototype(prototype, this); + + this.valid = blocks.observable(true); + + this.isLoading = blocks.observable(false); + + this.validationErrors = blocks.observable([]); + + this._isNew = false; + this._dataItem = dataItem || {}; // for original values + this._properties = Property.Inflate(this); + if (!this.options.baseUrl) { + this.options.baseUrl = application.options.baseUrl; + } + this.options.mode = DataSource.ObjectMode; + this._dataSource = new DataSource(this.options); + this._dataSource.on('change', this._onDataSourceChange, this); + this._dataSource.requestStart(function () { + _this.isLoading(true); + }); + this._dataSource.requestEnd(function () { + _this.isLoading(false); + }); + this._dataSource.on('sync', this._onDataSourceSync); + this.hasChanges = this._dataSource.hasChanges; + + this._ensurePropertiesCreated(dataItem); + this.init(); + } + + Model.prototype = { + /** + * The options for the Model + * + * @memberof Model + * @type {Object} + */ + options: {}, + + /** + * Override the init method to perform actions on creation for each Model instance + * + * @memberof Model + * @type {Function} + * + * @example {javascript} + * var App = blocks.Application(); + * + * var Product = App.Model({ + * init: function () { + * this.finalPrice = this.price() * this.ratio(); + * }, + * + * price: App.Property({ + * defaultValue: 0 + * }), + * + * ratio: App.Property({ + * defaultValue: 1 + * }) + * }); + */ + init: blocks.noop, + + /** + * Returns the `Collection` instance the model is part of. + * If it is not part of a collection it returns null. + * + * @returns {Collection|null} - The `Collection` or null. + * + * @example {javascript} + * var App = blocks.Application(); + * + * var User = App.Model({ + * init: function () { + * if (this.collection()) { + * this.collection().on('add remove', function handle() {}); + * } + * } + * }); + */ + collection: function () { + return this._collection || null; + }, + + /** + * Validates all observable properties that have validation and returns true if + * all values are valid otherwise returns false + * + * @memberof Model + * @returns {boolean} - Value indicating if the model is valid or not + * + * @example {javascript} + * var App = blocks.Application(); + * + * var User = App.Model({ + * username: App.Property({ + * required: true + * }), + * + * email: App.Property({ + * email: true + * }) + * }); + * + * App.View('SignUp', { + * newUser: User(), + * + * registerUser: function () { + * if (this.newUser.validate()) { + * alert('Successful registration!'); + * } + * } + * }); + */ + validate: function () { + var properties = this._properties; + var isValid = true; + var property; + var key; + + for (key in properties) { + property = this[key]; + if (blocks.isObservable(property) && blocks.isFunction(property.validate) && !property.validate()) { + isValid = false; + } + } + this.valid(isValid); + this._updateValidationErrors(); + return isValid; + }, + + /** + * Extracts the raw(non observable) dataItem object values from the Model + * + * @memberof Model + * @returns {Object} - Returns the raw dataItem object + * + * @example {javascript} + * var App = blocks.Application(); + * var User = App.Model({ + * firstName: App.Property({ + * defaultValue: 'John' + * }) + * }); + * + * App.View('Profile', { + * user: User(), + * + * init: function () { + * var dataItem = this.user.dataItem(); + * // -> { firstName: 'defaultValue' } + * } + * }); + */ + dataItem: function () { + var properties = this._properties; + var dataItem = {}; + var key; + var property; + + for (key in properties) { + property = properties[key]; + if (key != '__id__' && blocks.isFunction(this[property.propertyName])) { + dataItem[property.field || property.propertyName] = this[property.propertyName](); + } + } + if (this.isNew()) { + delete dataItem[this.options.idAttr]; + } + + return dataItem; + }, + + /** + * Applies new properties to the Model by providing an Object + * + * @memberof Model + * @param {Object} dataItem - The object from which the new values will be applied + * @returns {Model} - Chainable. Returns itself + */ + reset: function (dataItem) { + this._ensurePropertiesCreated(dataItem); + return this; + }, + + /** + * Determines whether the instance is new. If true when syncing the item will send + * for insertion instead of updating it. The check is determined by the idAttr value + * specified in the options. If idAttr is not specified the item will always be considered new. + * + * @memberof Model + * @returns {boolean} - Returns whether the instance is new + */ + isNew: function () { + var idAttr = this.options.idAttr; + var value = blocks.unwrap(this[idAttr]); + var property = this._properties[idAttr]; + + if ((!value && value !== 0) || (property && value === property.defaultValue)) { + return true; + } + return false; + }, + + /** + * Fires a request to the server to populate the Model based on the read URL specified + * + * @memberof Model + * @param {Object} [params] - The parameters Object that will be used to populate the + * Model from the specified options.read URL. If the URL does not contain parameters + * @returns {Model} - Chainable. Returns the Model itself - returns this; + */ + read: function (params, callback) { + // TODO: Write tests for the callback checking if it is beeing called + if (blocks.isFunction(params)) { + callback = params; + params = undefined; + } + this._dataSource.read({ + data: params + }, callback); + return this; + }, + + + destroy: function (removeFromCollection) { + removeFromCollection = removeFromCollection === false ? false : true; + if (removeFromCollection && this._collection) { + this._collection.remove(this); + } + this._dataSource._remove([this.dataItem()]); + return this; + }, + + /** + * Synchronizes the changes with the server by sending requests to the provided URL's + * + * @memberof Model + * @param {Function} [callback] - Optional callback which will be executed + * when all sync operations have been successfully completed + * @returns {Model} - Returns the Model itself - return this; + */ + sync: function (callback) { + if (this.isNew()) { + this._dataSource.data.add(this.dataItem()); + } + this._dataSource.sync(callback); + return this; + }, + + clone: function () { + return new this.constructor(blocks.clone(this._initialDataItem, true)); + }, + + _setPropertyValue: function (property, propertyValue) { + var propertyName = property.propertyName; + if (blocks.isFunction(this[propertyName])) { + this[propertyName](propertyValue); + this._dataSource.update(this.dataItem()); + } else if (property.isObservable) { + this[propertyName] = this._createObservable(property, propertyValue); + } else { + this[propertyName] = function () { + return propertyValue; + }; + } + }, + + _ensurePropertiesCreated: function (dataItem) { + var properties = this._properties; + var property; + var key; + var field; + + if (dataItem) { + if (Model.prototype.isPrototypeOf(dataItem)) { + dataItem = dataItem.dataItem(); + } + + for (key in dataItem) { + property = properties[key]; + if (!property) { + property = properties[key] = blocks.extend({}, this._application.Property.Defaults()); + property.propertyName = key; + } + this._setPropertyValue(property, dataItem[key]); + } + } + + for (key in properties) { + property = properties[key]; + if (!blocks.has(dataItem, property.propertyName)) { + field = property.field || property.propertyName; + this._setPropertyValue(property, property.value || (blocks.has(dataItem, field) ? dataItem[field] : property.defaultValue)); + } + } + }, + + _createObservable: function (property, value) { + var _this = this; + var properties = this._properties; + var observable = Property.Create(property, this, value); + + observable + .on('change', function () { + if (!_this.isNew()) { + _this._dataSource.update(_this.dataItem()); + } + }) + .on('validate', function () { + var isValid = true; + var key; + for (key in properties) { + if (blocks.isFunction(_this[key].valid) && !_this[key].valid()) { + isValid = false; + break; + } + } + _this._updateValidationErrors(); + _this.valid(isValid); + }); + + if (!this._collection) { + observable.extend(); + } + return observable; + }, + + _onDataSourceChange: function () { + var dataItem = blocks.unwrapObservable(this._dataSource.data())[0]; + this._ensurePropertiesCreated(dataItem); + }, + + _updateValidationErrors: function () { + var properties = this._properties; + var result = []; + var value; + var key; + + for (key in properties) { + value = this[key]; + if (value.errorMessages) { + result.push.apply(result, value.errorMessages()); + } + } + + this.validationErrors.reset(result); + } + }; + + if (blocks.core.expressionsCreated) { + blocks.core.applyExpressions('object', Model.prototype); + } + + + function clonePrototype(prototype, object) { + var key; + var value; + + for (key in prototype) { + value = prototype[key]; + if (Property.Is(value)) { + continue; + } + + if (blocks.isObservable(value)) { + // clone the observable and also its value by passing true to the clone method + object[key] = value.clone(true); + object[key].__context__ = object; + } else if (blocks.isFunction(value)) { + object[key] = blocks.bind(value, object); + } else if (Model.prototype.isPrototypeOf(value)) { + object[key] = value.clone(true); + } else if (blocks.isObject(value) && !blocks.isPlainObject(value)) { + object[key] = blocks.clone(value, true); + } else { + object[key] = blocks.clone(value, true); + } + } + } + + var routeStripper = /^[#\/]|\s+$/g; + var rootStripper = /^\/+|\/+$/g; + var isExplorer = /msie [\w.]+/; + var trailingSlash = /\/$/; + var pathStripper = /[?#].*$/; + var HASH = 'hash'; + var PUSH_STATE = 'pushState'; + + function History(options) { + this._options = blocks.extend({ + root: '/' + }, options); + + this._tryFixOrigin(); + + this._initial = true; + this._location = window.location; + this._history = window.history; + this._root = ('/' + this._options.root + '/').replace(rootStripper, '/'); + this._interval = 50; + this._fragment = this._getFragment(); + this._wants = this._options.history === true ? HASH : this._options.history; + this._use = this._wants == PUSH_STATE && (this._history && this._history.pushState) ? PUSH_STATE : HASH; + this._hostRegEx = new RegExp(escapeRegEx(this._location.host)); + } + + History.prototype = { + start: function () { + var fragment = this._fragment; + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + if (this._use == HASH && oldIE) { + this._createIFrame(); + this.navigate(fragment); + } + + this._initEvents(oldIE); + if (!this._tryAdaptMechanism(fragment)) { + this._loadUrl(); + } + }, + + navigate: function (fragment, options) { + if (!options || options === true) { + options = { + trigger: !!options + }; + } + var url = this._root + (fragment = this._getFragment(fragment || '')); + var use = this._use; + var iframe = this._iframe; + var location = this._location; + + fragment = fragment.replace(pathStripper, ''); + if (this._fragment === fragment) { + return false; + } + this._fragment = fragment; + if (fragment === '' && url !== '/') { + url = url.slice(0, -1); + } + + if (this._wants == PUSH_STATE && use == PUSH_STATE) { + this._history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); + } else if (use == HASH) { + this._updateHash(location, fragment, options.replace); + if (iframe && (fragment !== this.getFragment(this._getHash(iframe)))) { + if (!options.replace) { + iframe.document.open().close(); + } + this._updateHash(iframe.location, fragment, options.replace); + } + } else { + location.assign(url); + return true; + } + + return this._loadUrl(fragment); + }, + + _initEvents: function (oldIE) { + var use = this._use; + var onUrlChanged = blocks.bind(this._onUrlChanged, this); + + if (this._wants == PUSH_STATE) { + addListener(document, 'click', blocks.bind(this._onDocumentClick, this)); + } + + if (use == PUSH_STATE) { + addListener(window, 'popstate', onUrlChanged); + } else if (use == HASH && !oldIE && ('onhashchange' in window)) { + addListener(window, 'hashchange', onUrlChanged); + } else if (use == HASH) { + this._checkUrlInterval = setInterval(onUrlChanged, this._interval); + } + }, + + _loadUrl: function (fragment) { + var initial = this._initial; + + this._initial = false; + this._fragment = fragment = this._getFragment(fragment); + + return Events.trigger(this, 'urlChange', { + url: fragment, + initial: initial + }); + }, + + _getHash: function (window) { + var match = (window ? window.location : this._location).href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + _getFragment: function (fragment) { + if (fragment == null) { + if (this._use == PUSH_STATE) { + var root = this._root.replace(trailingSlash, ''); + fragment = this._location.pathname; + if (!fragment.indexOf(root)) { + fragment = fragment.slice(root.length); + } + } else { + fragment = this._getHash(); + } + } + return fragment.replace(this._location.origin, '').replace(routeStripper, ''); + }, + + _onUrlChanged: function () { + var current = this._getFragment(); + if (current === this._fragment && this._iframe) { + current = this._getFragment(this._getHash(this._iframe)); + } + if (current === this._fragment) { + return false; + } + if (this._iframe) { + this.navigate(current); + } + this._loadUrl(); + }, + + _onDocumentClick: function (e) { + var target = e.target; + + while (target) { + if (target && target.tagName && target.tagName.toLowerCase() == 'a') { + var download = target.getAttribute('download'); + var element; + + if (download !== '' && !download && this._hostRegEx.test(target.href) && + !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey && e.which !== 2) { + + // handle click + if (this.navigate(target.href)) { + element = document.getElementById(window.location.hash.replace(/^#/, '')); + if (element && element.scrollIntoView) { + element.scrollIntoView(); + } + e.preventDefault(); + } + } + + break; + } + target = target.parentNode; + } + }, + + _tryAdaptMechanism: function (fragment) { + var root = this._root; + var use = this._use; + var location = this._location; + var atRoot = location.pathname.replace(/[^\/]$/, '$&/') === root; + + this._fragment = fragment; + if (this._wants == PUSH_STATE) { + if (use != PUSH_STATE && !atRoot) { + fragment = this._fragment = this._getFragment(null, true); + location.replace(root + location.search + '#' + fragment); + return true; + } else if (use == PUSH_STATE && atRoot && location.hash) { + this._fragment = this._getHash().replace(routeStripper, ''); + this._history.replaceState({}, document.title, root + fragment + location.search); + } + } + }, + + _updateHash: function (location, fragment, replace) { + if (replace) { + var href = location.href.replace(/(javascript:|#).*$/, ''); + location.replace(href + '#' + fragment); + } else { + location.hash = '#' + fragment; + } + }, + + _createIFrame: function () { + /* jshint scripturl: true */ + var iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.src = 'javascript:0'; + iframe.tabIndex = -1; + document.body.appendChild(iframe); + this._iframe = iframe.contentWindow; + }, + + _tryFixOrigin: function () { + var location = window.location; + if (!location.origin) { + location.origin = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port: ''); + } + } + }; + + Events.register(History.prototype, ['on']); + + /** + * @namespace Collection + */ + function Collection(ModelType, prototype, application, initialData) { + return createCollectionObservable(ModelType, prototype, application, initialData); + } + + blocks.observable.remote = function (options) { + return createCollectionObservable(null, { + options: options + }, null, this.__value__); + }; + + function createCollectionObservable(ModelType, prototype, application, initialData) { + var observable = blocks.observable([]).extend(); + var properties = Property.Inflate(prototype); + var key; + + for (key in properties) { + observable[key] = properties[key]; + } + + observable._baseUpdate = observable.update; + blocks.each(blocks.observable.fn.collection, function (value, key) { + if (blocks.isFunction(value) && key.indexOf('_') !== 0) { + observable[key] = blocks.bind(observable[key], observable); + } + }); + blocks.extend(observable, blocks.observable.fn.collection, prototype); + clonePrototype(prototype, observable); + observable._Model = ModelType; + observable._prototype = prototype; + + if (application) { + observable._application = application; + observable._view = blocks.__viewInInitialize__; + if (!prototype.options.baseUrl) { + prototype.options.baseUrl = application.options.baseUrl; + } + } + + observable._dataSource = new DataSource(prototype.options); + observable._dataSource.on('change', observable._onDataSourceChange, observable); + observable.hasChanges = observable._dataSource.hasChanges; + if (ModelType) { + observable.on('adding', observable._onAdding, observable); + observable.on('remove add', observable._onChange, observable); + } + + if (blocks.isArray(initialData)) { + observable.reset(initialData); + } + + if (prototype.init) { + prototype.init.call(observable); + } + + return observable; + } + + blocks.observable.fn.collection = { + + /** + * Fires a request to the server to populate the Model based on the read URL specified + * + * @memberof Collection + * @param {Object} [params] - The parameters Object that will be used to populate the + * Collection from the specified options.read URL. If the URL does not contain parameters + * @returns {Collection} - Chainable. Returns the Collection itself - return this; + * + * @example {javascript} + * var App = blocks.Application(); + * + * var Products = App.Collection({ + * options: { + * read: { + * url: 'http://your-servrice-url/{{id}}' + * } + * } + * }); + * + * var products = Products().read({ + * // the id that will be replaced in the above options.read URL + * id: 3 + * }); + */ + read: function (params, callback) { + // TODO: Write tests for the callback checking if it is being called + var _this = this; + + if (blocks.isFunction(params)) { + callback = params; + params = undefined; + } + this._dataSource.read({ + data: params + }, callback ? function () { + callback.call(_this.__context__); + } : blocks.noop); + + return this; + }, + + /** + * Clear all changes made to the collection + * + * @memberof Collection + * @returns {Collection} Chainable. Returns this + * + * @example {javascript} + * var App = blocks.Application(); + * + * var Products = App.Collection({ + * + * }); + * + * App.View('Products', function () { + * products: Products(), + * + * init: function () { + * this.products.push({ + * ProductName: 'Fish' + * }); + * + * // -> this.products.length = 1 + * this.products.clearChanges(); + * // -> this.products.length = 0 + * } + * }); + */ + clearChanges: function () { + this._dataSource.clearChanges(); + return this; + }, + + /** + * Performs an ajax request for all create, update and delete operations in order to sync them + * with a database. + * + * @memberof Collection + * @param {Function} [callback] - Optional callback which will be executed + * when all sync operations have been successfully completed + * @returns {Collection} - Chainable. Returns the Collection itself - return this; + * + * @example {javascript} + * var App = blocks.Application(); + * var Products = App.Collection({ + * options: { + * create: { + * url: 'serviceURL/CreateProduct' + * } + * } + * }); + * + * App.View('Products', function () { + * products: Products(), + * + * init: function () { + * this.products.push({ + * ProductName: 'Fish' + * }); + * + * // sends AJAX request to the create.url with the new item + * this.products.sync(); + * } + * }); + */ + sync: function (callback) { + this._dataSource.sync(callback); + return this; + }, + + /** + * + * + * @memberof Collection + * @param {number} id - + * @param {Object} newValues - + * @returns {Collection} - Chainable. Returns the Collection itself - return this; + */ + update: function (id, newValues) { + if (arguments.length === 0) { + this._baseUpdate.call(this); + } else { + this._dataSource.update(id, newValues); + } + return this; + }, + + sortBy: function (callback, thisArg) { + if (typeof callback == 'string') { + var fieldName = callback; + callback = function (value) { + return value[fieldName](); + }; + } + blocks.sortBy(this.__value__, callback, thisArg); + return this; + }, + + clone: function (cloneValue) { + return createCollectionObservable( + this._Model, + this._prototype, + this._application, + cloneValue ? blocks.clone(this.__value__) : this.__value__); + }, + + // TODO: Add a test which adds to the center of the collection or the start + // startIndex = args.index, + _onAdding: function (args) { + var _this = this; + var ModelType = this._Model; + var items = args.items; + + blocks.each(items, function (item, index) { + if (Model.prototype.isPrototypeOf(item)) { + item = item.dataItem(); + } + items[index] = new ModelType(item, _this); + }); + }, + + _onChange: function (args) { + var type = args.type; + var items = args.items; + var newItems = []; + var i = 0; + var item; + + if (this._internalChanging) { + return; + } + + for (; i < items.length; i++) { + item = items[i]; + if (item && (type == 'remove' || (type == 'add' && item.isNew()))) { + newItems.push(item.dataItem()); + } + } + + if (type == 'remove') { + this._dataSource.data.removeAt(args.index, args.items.length); + } else if (type == 'add') { + this._dataSource.data.addMany(newItems); + } + }, + + _onDataSourceChange: function () { + this._internalChanging = true; + this.reset(this._dataSource.data()); + this._internalChanging = false; + this.clearChanges(); + if (this._view) { + this._view.trigger('ready'); + } + } + }; + + /** + * @namespace View + */ + function View(application, parentView) { + var _this = this; + + this._bindContext(); + this._views = []; + this._application = application; + this._parentView = parentView || null; + this._initCalled = false; + this._html = undefined; + + this.loading = blocks.observable(false); + this.isActive = blocks.observable(!blocks.has(this.options, 'route')); + this.isActive.on('changing', function (oldValue, newValue) { + _this._tryInitialize(newValue); + }); + + if (this.options.preload || this.isActive()) { + this._load(); + } + } + + View.prototype = { + /** + * Determines if the view is visible or not. + * This property is automatically populated when routing is enabled for the view. + * + * @memberof View + * @name isActive + * @type {blocks.observable} + */ + + /** + * Override the init method to perform actions when the View is first created + * and shown on the page + * + * @memberof View + * @type {Function} + * + * @example {javascript} + * var App = blocks.Application(); + * + * App.View('Statistics', { + * init: function () { + * this.loadRemoteData(); + * }, + * + * loadRemoteData: function () { + * // ...stuff... + * } + * }); + */ + init: blocks.noop, + + /** + * Override the ready method to perform actions when the DOM is ready and + * all data-query have been executed. + * + * @memberof View + * @type {Function} + * + * @example {javascript} + * var App = blocks.Application(); + * + * App.View('ContactUs', { + * ready: function () { + * $('#contact-form').ajaxSubmit(); + * } + * }); + */ + ready: blocks.noop, + + /** + * Override the routed method to perform actions when the View have routing and routing + * mechanism actives it. + * + * @memberof View + * @type {Function} + * + * @example {javascript} + * var App = blocks.Application(); + * + * App.View('ContactUs', { + * options: { + * route: 'contactus' + * }, + * + * routed: function () { + * alert('Navigated to ContactUs page!') + * } + * }); + */ + routed: blocks.noop, + + /** + * Observable which value is true when the View html + * is being loaded using ajax request. It could be used + * to show a loading indicator. + * + * @memberof View + */ + loading: blocks.observable(false), + + /** + * Gets the parent view. + * Returns null if the view is not a child of another view. + * + * @memberof View + */ + parentView: function () { + return this._parentView; + }, + + /** + * Routes to a specific URL and actives the appropriate views associated with the URL + * + * @memberof View + * @param {String} name - + * @returns {View} - Chainable. Returns this + * + * @example {javascript} + * var App = blocks.Application(); + * + * App.View('ContactUs', { + * options: { + * route: 'contactus' + * } + * }); + * + * App.View('Navigation', { + * navigateToContactUs: function () { + * this.route('contactus') + * } + * }); + */ + route: function (/* name */ /*, ...params */) { + this._application._history.navigate(blocks.toArray(arguments).join('/')); + return this; + }, + + navigateTo: function (view, params) { + this._application.navigateTo(view, params); + }, + + _bindContext: function () { + var key; + var value; + + for (key in this) { + value = this[key]; + + if (blocks.isObservable(value)) { + value.__context__ = this; + } else if (blocks.isFunction(value)) { + this[key] = blocks.bind(value, this); + } + } + }, + + _tryInitialize: function (isActive) { + if (!this._initialized && isActive) { + if (this.options.url && !this._html) { + this._callInit(); + this._load(); + } else { + this._initialized = true; + this._callInit(); + if (this.isActive()) { + this.isActive.update(); + } + } + } + }, + + _routed: function (params, metadata) { + this._tryInitialize(true); + this.routed(params, metadata); + blocks.each(this._views, function (view) { + if (!view.options.route) { + view._routed(params, metadata); + } + }); + this.isActive(true); + }, + + _callInit: function () { + if (this._initCalled) { + return; + } + + var key; + var value; + + blocks.__viewInInitialize__ = this; + for (key in this) { + value = this[key]; + if (blocks.isObservable(value)) { + value.__context__ = this; + } + } + this.init(); + blocks.__viewInInitialize__ = undefined; + this._initCalled = true; + }, + + _load: function () { + var url = this.options.url; + var serverData = this._application._serverData; + + if (serverData && serverData.views && serverData.views[url]) { + url = this.options.url = undefined; + this._tryInitialize(true); + } + + if (url && !this.loading()) { + this.loading(true); + ajax({ + isView: true, + url: url, + success: blocks.bind(this._loaded, this), + error: blocks.bind(this._error, this) + }); + } + }, + + _loaded: function (html) { + this._html = html; + this._tryInitialize(true); + this.loading(false); + }, + + _error: function () { + this.loading(false); + } + }; + + Events.register(View.prototype, ['on', 'off', 'trigger']); + + { + blocks.debug.addType('View', function (value) { + if (value && View.prototype.isPrototypeOf(value)) { + return true; + } + return false; + }); + } var application; + blocks.Application = function (options) { + return (application = application || new Application(options)); + }; + + blocks.core.deleteApplication = function () { + application = undefined; + }; + + /** + * MVC Application Class + * + * @namespace Application + * @module mvc + * @param {Object} options - The options for the application + */ + function Application(options) { + this._router = new Router(this); + this._modelPrototypes = {}; + this._collectionPrototypes = {}; + this._viewPrototypes = {}; + this._views = {}; + this._currentRoutedView = undefined; + this._started = false; + this.options = blocks.extend({}, this.options, options); + this._serverData = null; + + this._setDefaults(); + + this._prepare(); + } + + Application.prototype = { + options: { + history: true + }, + + /** + * Creates an application property for a Model + * + * @memberof Application + * @param {Object)} property - An object describing the options for the current property + * + * @example {javascript} + * + * var App = blocks.Application(); + * + * var User = App.Model({ + * username: App.Property({ + * defaultValue: 'John Doe' + * }) + * }); + */ + Property: function (property) { + if (blocks.isString(property)) { + return function () { + return this[property](); + }; + } else { + property = blocks.extend({}, this.Property.Defaults(), property); + property = new Property(property); + + return property; + } + }, + + /** + * Creates a new Model + * + * @memberof Application + * @param {Object} prototype - the Model object properties that will be created + * @returns {Model} - the Model type with the specified properties + * @example {javascript} + * + * var App = blocks.Application(); + * + * var User = App.Model({ + * firstName: App.Property({ + * required: true, + * validateOnChange: true + * }), + * + * lastName: App.Property({ + * required: true, + * validateOnChange: true + * }), + * + * fullName: App.Property({ + * value: function () { + * return this.firstName() + ' ' + this.lastName(); + * } + * }) + * }); + * + * App.View('Profile', { + * user: User({ + * firstName: 'John', + * lastName: 'Doe' + * }) + * }); + * + * @example {html} + *
+ *

+ * FullName is: {{user.fullName()}} + *

+ *
+ * + * + *
+ *

+ * FullName is: John Doe + *

+ *
+ */ + Model: function (prototype) { + var _this = this; + var ExtendedModel = function (dataItem, collection) { + if (!Model.prototype.isPrototypeOf(this)) { + return new ExtendedModel(dataItem, collection); + } + this._super([_this, prototype, dataItem, collection]); + }; + + prototype = prototype || {}; + prototype.options = prototype.options || {}; + + return blocks.inherit(Model, ExtendedModel, prototype); + }, + + /** + * Creates a new Collection + * + * @memberof Application + * @param {Object} prototype - The Collection object properties that will be created. + * @returns {Collection} - The Collection type with the specified properties + * @example {javascript} + * + * var App = blocks.Application(); + * + * var User = App.Model({ + * firstName: App.Property({ + * required: true, + * validateOnChange: true + * }), + * + * lastName: App.Property({ + * required: true, + * validateOnChange: true + * }), + * + * fullName: App.Property({ + * value: function () { + * return this.firstName() + ' ' + this.lastName(); + * } + * }) + * }); + * + * var Users = App.Collection(User, { + * count: App.Property({ + * value: function () { + * return this().length; + * } + * }) + * }); + * + * App.View('Profiles', { + * users: Users([{ + * firstName: 'John', + * lastName: 'Doe' + * }, { + * firstName: 'Johna', + * lastName: 'Doa' + * }]) + * }); + * + * @example {html} + *
+ *

Total count is {{users.count}}

+ * + *
+ * + * + *
+ *

Total count is 2

+ * + *
+ */ + Collection: function (ModelType, prototype) { + var _this = this; + var ExtendedCollection = function (initialData) { + if (!Collection.prototype.isPrototypeOf(this)) { + return new ExtendedCollection(initialData); + } + return this._super([ModelType, prototype, _this, initialData]); + }; + + if (!ModelType) { + ModelType = this.Model(); + } else if (!Model.prototype.isPrototypeOf(ModelType.prototype)) { + prototype = ModelType; + ModelType = this.Model(); + } + prototype = prototype || {}; + prototype.options = prototype.options || {}; + + return blocks.inherit(Collection, ExtendedCollection, prototype); + }, + + /** + * Defines a view that will be part of the Application + * + * @memberof Application + * @param {string} [parentViewName] - Provide this parameter only if you are creating nested views. + * This is the name of the parent View + * @param {string} name - The name of the View you are creating + * @param {Object} prototype - The object that will represent the View + * + * @example {javascript} + * var App = blocks.Application(); + * + * App.View('Clicker', { + * handleClick: function () { + * alert('Clicky! Click!'); + * } + * }); + * + * @example {html} + * + *
+ *

Click here!

+ *
+ */ + View: function (name, prototype, nestedViewPrototype) { + // TODO: Validate prototype by checking if a property does not override a proto method + // if the prototype[propertyName] Type eqals the proto[propertyName] Type do not throw error + if (arguments.length == 1) { + return this._views[name]; + } + if (blocks.isString(prototype)) { + this._viewPrototypes[prototype] = this._createView(nestedViewPrototype); + nestedViewPrototype.options.parentView = name; + } else { + this._viewPrototypes[name] = this._createView(prototype); + } + }, + + extend: function (obj) { + blocks.extend(this, obj); + clonePrototype(obj, this); + return this; + }, + + navigateTo: function (view, params) { + if (!view.options.route) { + return false; + } + this._history.navigate(this._router.routeTo(view.options.routeName, params)); + return true; + }, + + start: function (element) { + if (!this._started) { + this._started = true; + this._serverData = window.__blocksServerData__; + this._createViews(); + blocks.domReady(blocks.bind(this._ready, this, element)); + } + }, + + _prepare: function () { + blocks.domReady(function () { + setTimeout(blocks.bind(function () { + this.start(); + }, this)); + }, this); + }, + + _startHistory: function () { + this._history = new History(this.options); + this._history + .on('urlChange', blocks.bind(this._urlChange, this)) + .start(); + }, + + _ready: function (element) { + this._serverData = window.__blocksServerData__; + this._startHistory(); + blocks.query(this, element); + this._viewsReady(this._views); + }, + + _viewsReady: function (views) { + var callReady = this._callReady; + + blocks.each(views, function (view) { + if (view.ready !== blocks.noop) { + if (view.isActive()) { + callReady(view); + } else { + view.isActive.once('change', function () { + callReady(view); + }); + } + } + }); + }, + + _callReady: function (view) { + if (view.loading()) { + view.loading.once('change', function () { + view.ready(); + }); + } else { + view.ready(); + } + }, + + _urlChange: function (data) { + var _this = this; + var currentView = this._currentView; + var routes = this._router.routeFrom(data.url); + var found = false; + + blocks.each(routes, function (route) { + blocks.each(_this._views, function (view) { + if (view.options.routeName == route.id) { + if (!currentView && (view.options.initialPreload || + (data.initial && _this._serverData && _this.options.history == 'pushState'))) { + view.options.url = undefined; + } + if (currentView && currentView != view) { + currentView.isActive(false); + } + view._routed(route.params, data); + _this._currentView = view; + found = true; + return false; + } + }); + if (found) { + return false; + } + }); + + if (!found && currentView) { + currentView.isActive(false); + } + + return found; + }, + + _createView: function (prototype) { + prototype.options = blocks.extend({}, this.View.Defaults(), prototype.options); + // if (prototype.options.route) { + // prototype.options.routeName = this._router.registerRoute(prototype.options.route); + // } + + return blocks.inherit(View, function (application, parentView) { + this._super([application, parentView]); + }, prototype); + }, + + _createViews: function () { + var viewPrototypePairs = blocks.pairs(this._viewPrototypes); + var views = this._views; + var viewsInOrder = []; + var pair; + var View; + var parentViewName; + var currentView; + var i = 0; + + while (viewPrototypePairs.length !== 0) { + for (; i < viewPrototypePairs.length; i++) { + pair = viewPrototypePairs[i]; + View = pair.value; + parentViewName = View.prototype.options.parentView; + if (parentViewName) { + //#region blocks + if (!this._viewPrototypes[parentViewName]) { + viewPrototypePairs.splice(i, 1); + i--; + throw new Error('View with ' + parentViewName + 'does not exist'); + //TODO: Throw critical error parentView with such name does not exists + } + //#endregion + if (views[parentViewName]) { + currentView = new View(this, views[parentViewName]); + views[parentViewName][pair.key] = currentView; + views[parentViewName]._views.push(currentView); + if (!currentView.parentView().isActive()) { + currentView.isActive(false); + } + viewPrototypePairs.splice(i, 1); + i--; + } + } else { + currentView = new View(this); + this[pair.key] = currentView; + viewPrototypePairs.splice(i, 1); + i--; + parentViewName = undefined; + } + + if (currentView) { + if (blocks.has(currentView.options, 'route')) { + currentView.options.routeName = this._router.registerRoute( + currentView.options.route, this._getParentRouteName(currentView)); + } + views[pair.key] = currentView; + viewsInOrder.push(currentView); + } + } + } + + for (i = 0; i < viewsInOrder.length; i++) { + viewsInOrder[i]._tryInitialize(viewsInOrder[i].isActive()); + } + + this._viewPrototypes = undefined; + }, + + _getParentRouteName: function (view) { + while (view) { + if (view.options.routeName) { + return view.options.routeName; + } + view = view.parentView(); + } + }, + + _setDefaults: function () { + this.Model.Defaults = blocks.observable({ + options: {} + }).extend(); + + this.Collection.Defaults = blocks.observable({ + options: {} + }).extend(); + + this.Property.Defaults = blocks.observable({ + isObservable: true, + maxErrors: 1 + }).extend(); + + this.View.Defaults = blocks.observable({ + options: { } + }).extend(); + } + }; + + + + + +})();// @source-code + })(); + + (function() { + var toString = blocks.toString; + blocks.toString = function(value) { + if (arguments.length === 0) { + return 'jsblocks - Better MV-ish Framework'; + } + return toString(value); + }; + })(); + var _blocks = global.blocks; + + blocks.noConflict = function (deep) { + if (global.blocks === blocks) { + global.blocks = _blocks; + } + + if (deep && global.blocks === blocks) { + global.blocks = _blocks; + } + + return blocks; + }; + + if (typeof define === 'function' && define.amd) { + define('blocks', [], function () { + return blocks; + }); + } + + if (noGlobal !== true) { + global.blocks = blocks; + } + + return blocks; + +})); diff --git a/examples/jsblocks/node_modules/todomvc-app-css/index.css b/examples/jsblocks/node_modules/todomvc-app-css/index.css new file mode 100644 index 0000000000..ba79a58df3 --- /dev/null +++ b/examples/jsblocks/node_modules/todomvc-app-css/index.css @@ -0,0 +1,378 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; +} + +button, +input[type="checkbox"] { + outline: none; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +label[for='toggle-all'] { + display: none; +} + +.toggle-all { + position: absolute; + top: -55px; + left: -12px; + width: 60px; + height: 34px; + text-align: center; + border: none; /* Mobile Safari */ +} + +.toggle-all:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle:after { + content: url('data:image/svg+xml;utf8,'); +} + +.todo-list li .toggle:checked:after { + content: url('data:image/svg+xml;utf8,'); +} + +.todo-list li label { + white-space: pre; + word-break: break-word; + padding: 15px 60px 15px 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a.selected, +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + position: relative; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } + + .toggle-all { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} diff --git a/examples/jsblocks/node_modules/todomvc-common/base.css b/examples/jsblocks/node_modules/todomvc-common/base.css new file mode 100644 index 0000000000..da65968a73 --- /dev/null +++ b/examples/jsblocks/node_modules/todomvc-common/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} diff --git a/examples/jsblocks/node_modules/todomvc-common/base.js b/examples/jsblocks/node_modules/todomvc-common/base.js new file mode 100644 index 0000000000..3c6723f390 --- /dev/null +++ b/examples/jsblocks/node_modules/todomvc-common/base.js @@ -0,0 +1,249 @@ +/* global _ */ +(function () { + 'use strict'; + + /* jshint ignore:start */ + // Underscore's Template Module + // Courtesy of underscorejs.org + var _ = (function (_) { + _.defaults = function (object) { + if (!object) { + return object; + } + for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { + var iterable = arguments[argsIndex]; + if (iterable) { + for (var key in iterable) { + if (object[key] == null) { + object[key] = iterable[key]; + } + } + } + } + return object; + } + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + return _; + })({}); + + if (location.hostname === 'todomvc.com') { + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + ga('create', 'UA-31081062-1', 'auto'); + ga('send', 'pageview'); + } + /* jshint ignore:end */ + + function redirect() { + if (location.hostname === 'tastejs.github.io') { + location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com'); + } + } + + function findRoot() { + var base = location.href.indexOf('examples/'); + return location.href.substr(0, base); + } + + function getFile(file, callback) { + if (!location.host) { + return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.'); + } + + var xhr = new XMLHttpRequest(); + + xhr.open('GET', findRoot() + file, true); + xhr.send(); + + xhr.onload = function () { + if (xhr.status === 200 && callback) { + callback(xhr.responseText); + } + }; + } + + function Learn(learnJSON, config) { + if (!(this instanceof Learn)) { + return new Learn(learnJSON, config); + } + + var template, framework; + + if (typeof learnJSON !== 'object') { + try { + learnJSON = JSON.parse(learnJSON); + } catch (e) { + return; + } + } + + if (config) { + template = config.template; + framework = config.framework; + } + + if (!template && learnJSON.templates) { + template = learnJSON.templates.todomvc; + } + + if (!framework && document.querySelector('[data-framework]')) { + framework = document.querySelector('[data-framework]').dataset.framework; + } + + this.template = template; + + if (learnJSON.backend) { + this.frameworkJSON = learnJSON.backend; + this.frameworkJSON.issueLabel = framework; + this.append({ + backend: true + }); + } else if (learnJSON[framework]) { + this.frameworkJSON = learnJSON[framework]; + this.frameworkJSON.issueLabel = framework; + this.append(); + } + + this.fetchIssueCount(); + } + + Learn.prototype.append = function (opts) { + var aside = document.createElement('aside'); + aside.innerHTML = _.template(this.template, this.frameworkJSON); + aside.className = 'learn'; + + if (opts && opts.backend) { + // Remove demo link + var sourceLinks = aside.querySelector('.source-links'); + var heading = sourceLinks.firstElementChild; + var sourceLink = sourceLinks.lastElementChild; + // Correct link path + var href = sourceLink.getAttribute('href'); + sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http'))); + sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML; + } else { + // Localize demo links + var demoLinks = aside.querySelectorAll('.demo-link'); + Array.prototype.forEach.call(demoLinks, function (demoLink) { + if (demoLink.getAttribute('href').substr(0, 4) !== 'http') { + demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href')); + } + }); + } + + document.body.className = (document.body.className + ' learn-bar').trim(); + document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); + }; + + Learn.prototype.fetchIssueCount = function () { + var issueLink = document.getElementById('issue-count-link'); + if (issueLink) { + var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos'); + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onload = function (e) { + var parsedResponse = JSON.parse(e.target.responseText); + if (parsedResponse instanceof Array) { + var count = parsedResponse.length; + if (count !== 0) { + issueLink.innerHTML = 'This app has ' + count + ' open issues'; + document.getElementById('issue-count').style.display = 'inline'; + } + } + }; + xhr.send(); + } + }; + + redirect(); + getFile('learn.json', Learn); +})(); diff --git a/examples/jsblocks/package.json b/examples/jsblocks/package.json new file mode 100644 index 0000000000..32e86c61bd --- /dev/null +++ b/examples/jsblocks/package.json @@ -0,0 +1,8 @@ +{ + "private": true, + "dependencies": { + "blocks": "^0.3.4", + "todomvc-app-css": "^2.0.0", + "todomvc-common": "^1.0.0" + } +} diff --git a/examples/jsblocks/readme.md b/examples/jsblocks/readme.md new file mode 100644 index 0000000000..31485d5a35 --- /dev/null +++ b/examples/jsblocks/readme.md @@ -0,0 +1,29 @@ +# jsblocks TodoMVC Example + + > From simple user interfaces to complex single-page applications using faster, server-side rendered and easy to learn framework + > + > [jsblocks - jsblocks.com](http://jsblocks.com) + + +## Resources + +- [Website](http://jsblocks.com) +- [Documentation](http://jsblocks.com/learn) +- [Download](http://jsblocks.com/download) +- [GitHub Page](https://github.com/astoilkov/jsblocks) + +- [The jsblocks Framework via DailyJS](http://dailyjs.com/2015/05/29/the-jsblocks-framework/) + +### Support + +- [Gitter](https://gitter.im/astoilkov/jsblocks?utm_source=github_link) +- [Twitter](http://twitter.com/jsblocks) +- [StackOverflow](http://stackoverflow.com/questions/tagged/jsblocks) +- [Google Groups](https://groups.google.com/forum/#!forum/jsblocks) +- [Google+](https://plus.google.com/communities/100030562502977783693) + +*Let us [know](https://github.com/tastejs/todomvc/issues) if you discover anything worth sharing.* + +## Credit + +Created by [Antonio Stoilkov](http://astoilkov.com) diff --git a/index.html b/index.html index a94aeb62e1..7bd02390fe 100644 --- a/index.html +++ b/index.html @@ -284,6 +284,9 @@

Examples

  • Riot
  • +
  • + JSBlocks +
  • diff --git a/learn.json b/learn.json index 1ae73bf574..18433cef77 100644 --- a/learn.json +++ b/learn.json @@ -2231,6 +2231,43 @@ }] }}] }, + "jsblocks": { + "name": "jsblocks", + "description": "From simple user interfaces to complex single-page applications using faster, server-side rendered and easy to learn framework", + "homepage": "http://jsblocks.com", + "examples": [{ + "name": "Example", + "url": "examples/jsblocks" + }], + "link_groups": [{ + "heading": "Official Resources", + "links": [{ + "name": "Official Website", + "url": "http://jsblocks.com" + }, { + "name": "API Reference", + "url": "http://jsblocks.com/api" + }, { + "name": "Documentation", + "url": "http://jsblocks.com/learn" + }, { + "name": "Shopping Example", + "url": "https://github.com/astoilkov/jsblocks-shopping-example" + }] + }, { + "heading": "Community", + "links": [{ + "name": "jsblocks on Gitter", + "url": "https://gitter.im/astoilkov/jsblocks?utm_source=github_link" + }, { + "name": "jsblocks on Twitter", + "url": "http://twitter.com/jsblocks" + }, { + "name": "jsblocks on Stack Overflow", + "url": "http://stackoverflow.com/questions/tagged/jsblocks" + }] + }] + }, "templates": { "todomvc": "

    <%= name %>

    <% if (typeof examples !== 'undefined') { %> <% examples.forEach(function (example) { %>
    <%= example.name %>
    <% if (!location.href.match(example.url + '/')) { %> \" href=\"<%= example.url %>\">Demo, <% } if (example.type === 'backend') { %>\"><% } else { %>\"><% } %>Source <% }); %> <% } %>

    <%= description %>

    <% if (typeof link_groups !== 'undefined') { %>
    <% link_groups.forEach(function (link_group) { %>

    <%= link_group.heading %>

    <% }); %> <% } %> " }