// ConstraintJS (CJS) 0.9.8-beta2 // ConstraintJS may be freely distributed under the MIT License // http://cjs.from.so/ /* jslint nomen: true, vars: true */ /* jshint -W093 */ /* global document */ /** @expose cjs */ var cjs = (function (root) { "use strict"; // Utility functions // ----------------- // Many of the functions here are from http://underscorejs.org/ // Save bytes in the minified (but not gzipped) version: var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype, StringProto = String.prototype; // Create quick reference variables for speed access to core prototypes. var slice = ArrayProto.slice, toString = ObjProto.toString, concat = ArrayProto.concat, push = ArrayProto.push; // All **ECMAScript 5** native function implementations that we hope to use // are declared here. var nativeSome = ArrayProto.some, nativeIndexOf = ArrayProto.indexOf, nativeLastIndexOf = ArrayProto.lastIndexOf, nativeEvery = ArrayProto.every, nativeForEach = ArrayProto.forEach, nativeKeys = Object.keys, nativeFilter = ArrayProto.filter, nativeReduce = ArrayProto.reduce, nativeMap = ArrayProto.map, nativeTrim = StringProto.trim; //Bind a function to a context var bind = function (func, context) { return function () { return func.apply(context, arguments); }; }, bindArgs = function(func) { var args = rest(arguments, 1); return function() { return func.apply(this, args); }; }, trim = function(str){ return nativeTrim ? nativeTrim.call(str) : String(str).replace(/^\s+|\s+$/g, ''); }, doc = root.document, sTO = function(a,b) { return root.setTimeout(a,b); }, cTO = function(a,b) { return root.clearTimeout(a,b); }, // Binary and unary operators will be used for constraint modifiers and for templates, // which allow these operators to be used in constraints unary_operators = { "+": function (a) { return +a; }, "-": function (a) { return -a; }, "~": function (a) { return ~a; }, "!": function (a) { return !a; } }, binary_operators = {"===": function (a, b) { return a === b;}, "!==":function (a, b) { return a !== b; }, "==": function (a, b) { return a == b; }, "!=": function (a, b) { return a != b; }, ">": function (a, b) { return a > b; }, ">=": function (a, b) { return a >= b; }, "<": function (a, b) { return a < b; }, "<=": function (a, b) { return a <= b; }, "+": function (a, b) { return a + b; }, "-": function (a, b) { return a - b; }, "*": function (a, b) { return a * b; }, "/": function (a, b) { return a / b; }, "%": function (a, b) { return a % b; }, "^": function (a, b) { return a ^ b; }, "&&": function (a, b) { return a && b; }, "||": function (a, b) { return a || b; }, "&": function (a, b) { return a & b; }, "|": function (a, b) { return a | b; }, "<<": function (a, b) { return a << b; }, ">>": function (a, b) { return a >> b; }, ">>>": function (a, b) { return a >>> b;} }; var getTextContent, setTextContent; if(doc && !('textContent' in doc.createElement('div'))) { getTextContent = function(node) { return node && node.nodeType === 3 ? node.nodeValue : node.innerText; }; setTextContent = function(node, val) { if(node && node.nodeType === 3) { node.nodeValue = val; } else { node.innerText = val; } }; } else { getTextContent = function(node) { return node.textContent; }; setTextContent = function(node, val) { node.textContent = val; }; } var aEL = function(node, type, callback) { if(node.addEventListener) { node.addEventListener(type, callback); } else { node.attachEvent("on"+type, callback); } }, rEL = function(node, type, callback) { if(node.removeEventListener) { node.removeEventListener(type, callback); } else { node.detachEvent("on"+type, callback); } }; // Establish the object that gets returned to break out of a loop iteration. var breaker = {}; // Creating a unique id for constraints allows for quicker referencing var uniqueId = (function () { var id = 0; return function () { return id++; }; }()); // Create a (shallow-cloned) duplicate of an object. var clone = function(obj) { if (!isObject(obj)) { return obj; } return isArray(obj) ? obj.slice() : extend({}, obj); }; // Returns the keys of an object var keys = nativeKeys || function (obj) { if (obj !== Object(obj)) { throw new TypeError('Invalid object'); } var keys = [], key, len = 0; for (key in obj) { if (hOP.call(obj, key)) { keys[len++] = key; } } return keys; }; // Get the last element of an array. Passing **n** will return the last N // values in the array. var last = function(array, n) { if (!array) { return void 0; } else if (n===undefined) { return array[array.length - 1]; } else { return slice.call(array, Math.max(array.length - n, 0)); } }; // Determine if at least one element in the object matches a truth test. // Delegates to **ECMAScript 5**'s native `some` if available. var any = function(obj, iterator, context) { var result = false; if (!obj) { return result; } if (nativeSome && obj.some === nativeSome) { return obj.some(iterator, context); } each(obj, function(value, index, list) { if (result || (result = iterator.call(context, value, index, list))) { return breaker; } }); return !!result; }; // Returns everything but the first entry of the array. // Especially useful on the arguments object. Passing an **n** will return // the rest N values in the array. var rest = function(array, n) { return slice.call(array, n === undefined ? 1 : n); }; // Trim out all falsy values from an array. var compact = function(array) { return filter(array, identity); }; // Determine whether all of the elements match a truth test. // Delegates to **ECMAScript 5**'s native `every` if available. var every = function(obj, iterator, context) { iterator = iterator || identity; var result = true; if (!obj) { return result; } else if (nativeEvery && obj.every === nativeEvery) { return obj.every(iterator, context); } else { each(obj, function(value, index, list) { if (!(result = result && iterator.call(context, value, index, list))) { return breaker; } }); return !!result; } }; // Recursive call for flatten (from underscore) var recursiveFlatten = function(input, shallow, output) { if (shallow && every(input, isArray)) { return concat.apply(output, input); } each(input, function(value) { if (isArray(value) || isArguments(value)) { if(shallow) { push.apply(output, value); } else { recursiveFlatten(value, shallow, output); } } else { output.push(value); } }); return output; }; // Initial call to the recursive flatten function var flatten = function(input, shallow) { return recursiveFlatten(input, shallow, []); }; // Retrieve the values of an object's properties. var values = function (obj) { var values = []; var key; for (key in obj) { if (obj.hasOwnProperty(key)) { values.push(obj[key]); } } return values; }; // Is a given value a number? var isNumber = function (obj) { return toString.call(obj) === '[object Number]'; }, // Is a given value an array? // Delegates to ECMA5's native Array.isArray isArray = Array.isArray || function (obj) { return toString.call(obj) === '[object Array]'; }, // Is a given value a function? isFunction = function (obj) { return toString.call(obj) === '[object Function]'; }, // Is the given value a String? isString = function (obj) { return toString.call(obj) === '[object String]'; }, // Is a given variable an object? isObject = function (obj) { return obj === Object(obj); }, // Is a given value a DOM element? isElement = function(obj) { return !!(obj && obj.nodeType === 1); }, // Any element of any type? isAnyElement = function(obj) { return !!(obj && (obj.nodeType > 0)); }, // Is a given variable an arguments object? isArguments = function (obj) { return toString.call(obj) === '[object Arguments]'; }; // Keep the identity function around for default iterators. var identity = function (value) { return value; }; // Safely convert anything iterable into a real, live array. var toArray = function (obj) { if (!obj) { return []; } if (isArray(obj)) { return slice.call(obj); } if (isArguments(obj)) { return slice.call(obj); } if (obj.toArray && isFunction(obj.toArray)) { return obj.toArray(); } return map(obj, identity); }; // `hasOwnProperty` proxy, useful if you don't know if obj is null or not var hOP = ObjProto.hasOwnProperty, has = function (obj, key) { return hOP.call(obj, key); }, hasAny = function(obj) { return any(rest(arguments), function(x) { return has(obj, x); }); }; // Run through each element and calls `iterator` where `this` === `context` var each = function(obj, iterator, context) { var i, length; if (!obj) { return; } if (nativeForEach && obj.forEach === nativeForEach) { obj.forEach(iterator, context); } else if (obj.length === +obj.length) { i=0; length = obj.length; for (; i < length; i++) { if (iterator.call(context, obj[i], i, obj) === breaker) return; } } else { var kys = keys(obj); i=0; length = kys.length; for (; i < length; i++) { if (iterator.call(context, obj[kys[i]], kys[i], obj) === breaker) return; } } return obj; }; // Run through each element and calls 'iterator' where 'this' === context // and returns the return value for every element var map = function (obj, iterator, context) { var results = []; if (!obj) { return results; } if (nativeMap && obj.map === nativeMap) { return obj.map(iterator, context); } each(obj, function (value, index, list) { results[results.length] = iterator.call(context, value, index, list); }); if (obj.length === +obj.length) { results.length = obj.length; } return results; }; // Return all the elements that pass a truth test. // Delegates to **ECMAScript 5**'s native `filter` if available. var filter = function(obj, iterator, context) { var results = []; if (!obj) { return results; } if (nativeFilter && obj.filter === nativeFilter) { return obj.filter(iterator, context); } each(obj, function(value, index, list) { if (iterator.call(context, value, index, list)) { results.push(value); } }); return results; }; // Extend a given object with all the properties in passed-in object(s). var extend = function (obj) { each(slice.call(arguments, 1), function(source) { if (source) { for (var prop in source) { if(source.hasOwnProperty(prop)) { obj[prop] = source[prop]; } } } }); return obj; }; // Return the first item in arr where test is true var indexWhere = function (arr, test, start_index) { var i, len = arr.length; for (i = start_index || 0; i < len; i++) { if (test(arr[i], i)) { return i; } } return -1; }, lastIndexWhere = function(arr, test) { var i, len = arr.length; for (i = len-1; i >= 0; i--) { if (test(arr[i], i)) { return i; } } return -1; }; // The default equality check function var eqeqeq = function (a, b) { return a === b; }; // Return the first item in arr equal to item (where equality is defined in equality_check) var indexOf = function (arr, item, start_index, equality_check) { if(!equality_check && !start_index && nativeIndexOf && arr.indexOf === nativeIndexOf) { return arr.indexOf(item); } else { equality_check = equality_check || eqeqeq; return indexWhere(arr, function (x) { return equality_check(item, x); }, start_index); } }, lastIndexOf = function(arr, item, equality_check) { if(nativeLastIndexOf && arr.lastIndexOf === nativeLastIndexOf) { return arr.lastIndexOf(item); } else { equality_check = equality_check || eqeqeq; return lastIndexWhere(arr, function (x) { return equality_check(item, x); }); } }; // Remove an item in an array var remove = function (arr, obj) { return removeIndex(arr, indexOf(arr, obj)); }, removeIndex = function(arr, index) { if (index >= 0) { return arr.splice(index, 1)[0]; } return index; }; // Fold down a list of values into a single value var reduce = function(obj, iterator, memo) { var initial = arguments.length > 2; if (!obj) obj = []; if (nativeReduce && obj.reduce === nativeReduce) { return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); } each(obj, function(value, index, list) { memo = iterator(memo, value, index, list); }); return memo; }; var sparse_indexof = function(arr,item,start_index,equals) { //indexOf is wonky with sparse arrays var i = start_index,len = arr.length; while(i r ? pos : r, equality_check || eqeqeq); if (pos < 0) { pos = n; } symbols[s] = pos; return pos; }, indexed_lcs = function (x, y, equality_check) { var symbols = {}, r = 0, p = 0, p1, L = 0, idx, i, m = x.length, n = y.length, S = new Array(m < n ? n : m); if (n === 0 || m === 0) { return []; } p1 = popsym(0, x, y, symbols, r, n, equality_check); for (i = 0; i < m; i++) { p = (r === p) ? p1 : popsym(i, x, y, symbols, r, n, equality_check); p1 = popsym(i + 1, x, y, symbols, r, n, equality_check); if (p > p1) { i++; idx = p1; } else { idx = p; } if (idx === n || i === m) { p=popsym(i, x, y, symbols, r, n, equality_check); } else { r = idx; S[L] = {item: x[i], indicies: [i, idx]}; L++; } } return S.slice(0,L); }; // "Subtracts" `y` from `x` (takes `x-y`) and returns a list of items in `x` that aren't in `y` var diff = function (x, y, equality_check) { var i, j, xi, y_clone = clone(y), x_len = x.length, y_len = y.length, diff = [], diff_len = 0; // If there aren't any items, then the difference is the same as `x`. // not bothering to return a clone here because diff is private none of my code // modifies the return value if(y_len === 0 || x_len === 0) { return x; } for (i = 0; i < x_len; i += 1) { xi = x[i]; j = indexOf(y_clone, xi, 0, equality_check); if(j >= 0) { removeIndex(y_clone, j); // If there's nothing left to subtract, just add the rest of x to diff and return if(--y_len === 0) { diff.push.apply(diff, rest(x, i+1)); break; } } else { diff[diff_len] = xi; diff_len++; } } return diff; }; // Returns the items that are in both x and y, but also accounts for the count of equivalent items (as defined by equality_check) // Examples: // `x = [1,2,2,3]`, `y = [1,2,4]` -> `[1,2]`; // `x = [1,1,1]`, `y = [1,1]` -> `[1,1]` var dualized_intersection = function (x, y, equality_check) { var i, j, xi, y_clone = clone(y), x_len = x.length, y_len = y.length, intersection = []; for (i = 0; i < x_len && y_len > 0; i++) { xi = x[i]; j = indexOf(y_clone, xi, 0, equality_check); if(j >= 0) { intersection.push([xi, removeIndex(y_clone, j)]); y_len--; } } return intersection; }; // Utility functions for `array_source_map` var get_index_moved = function(info) { var item = info[1].item; return {item: item, from: info[0].index, to: info[1].index, from_item: info[0].item, to_item: item}; }, add_indicies = function(arr) { // suppose you have array `arr` defined by: // arr = []; arr[10] = 'hi'; // Looping through arr with arr.forEach (or cjs's map) would only produce the 10th item. // this function is declared to make sure every item is looped through var i = 0, len = arr.length, rv = []; while(i= 0) { info = indexed_added[info_index]; to_mappings[i] = { to: i, to_item: item, item: item }; } else if ((info_index = indexOf(moved_indicies, i)) >= 0) { info = indexed_moved[info_index]; to_mappings[i] = { to: i, to_item: item, item: item, from: info.from, from_item: info.from_item }; } else if ((info_index = indexOf(ics_indicies, i)) >= 0) { info = indexed_common_subsequence[info_index]; to_mappings[i] = { to: i, to_item: item, item: item, from: info.from, from_item: from[info.from] }; } i++; } return to_mappings.concat(map(indexed_removed, add_from_and_from_item)); }; // These utility functions help compute the array diff (without having to re-declare them every time get_array_diff is called var has_from = function(x) { return x.hasOwnProperty("from"); }, not_has_from = function(x) { return !has_from(x); }, has_to = function(x) { return x.hasOwnProperty("to"); }, not_has_to = function(x) { return !has_to(x); }, has_from_and_to = function(x) { return has_from(x) && has_to(x); }, unequal_from_to = function(x) { return has_from_and_to(x) && x.from !== x.to; }, sort_by_from_fn = function(a, b) { // This is equivalent to (but faster than): // if (a_has_from && b_has_from) { return a.from - b.from; } // else if (a_has_from && !b_has_from) { return -1; } // else if (!a_has_from && b_has_from) { return 1; } // else { return 0; } var a_has_from = has_from(a), b_has_from = has_from(b); return a_has_from && b_has_from ? a.from - b.from : b_has_from - a_has_from; }; /** * * `arrayDiff` returns an object with attributes: * `removed`, `added`, and `moved`. * Every item in `removed` has the format: `{item, index}` * Every item in `added` has the format: `{item, index}` * Every item in `moved` has the format: `{from_index, to_index}` * Every item in `index_changed` has the format: `{from_index, to_index}` * * When `oldArray` removes every item in `removed`, adds every item in `added`, * and moves every item in `moved` (in that order), it will result in an array * that is equivalent to `newArray`. Note: this function is used internally to * determine how to keep DOM nodes in sync with an underlying model with the * smallest number of modifications to the DOM tree. * * @method cjs.arrayDiff * @param {array[*]} from_val - The 'former' array * @param {array[*]} to_val - The 'new' array * @param {function} [equality_check] - A function that checks for equality between items * @return {Object} - added, removed, and moved items * * @example Taking the diff between `old_array` and `new_array` with the default equality check * * var old_array = ['a','b','c'], * new_array = ['c','b','d'], * diff = cjs.arrayDiff(old_array, new_array); * * // diff === { * // added: [ { item: 'd', to: 2, to_item: 'd' } ], * // removed: [ { from: 0, from_item: 'a' } ], * // moved: [ { item: 'c', from: 2, insert_at: 0, move_from: 1, to: 0 } ], * // index_changed: [ { from: 2, from_item: 'c', item: 'c', to: 0, to_item: 'c' } ] * // } * */ var get_array_diff = function (from_val, to_val, equality_check) { var source_map = array_source_map(from_val, to_val, equality_check), rearranged_array = clone(source_map).sort(sort_by_from_fn), added = filter(source_map, not_has_from), // back to front removed = filter(rearranged_array, not_has_to).reverse(), // back to front index_changed = filter(source_map, unequal_from_to), moved = []; each(removed, function (info) { removeIndex(rearranged_array, info.from); }); each(added, function (info) { rearranged_array.splice(info.to, 0, info); }); each(source_map, function (info, index) { if (has_from_and_to(info)) { if (rearranged_array[index] !== info) { var rearranged_array_info_index = indexOf(rearranged_array, info, index); rearranged_array.splice(index, 0, removeIndex(rearranged_array, rearranged_array_info_index)); moved.push({move_from: rearranged_array_info_index, insert_at: index, item: info.item, from: info.from, to: info.to}); } } }); rearranged_array = null; return { added: added, removed: removed, moved: moved, index_changed: index_changed , mapping: source_map}; }; // Convert dashed to camelCase; used by the css and data modules // Microsoft forgot to hump their vendor prefix (#9572) var rdashAlpha = /-([a-z]|[0-9])/ig, rmsPrefix = /^-ms-/, fcamelCase = function(all, letter) { return String(letter).toUpperCase(); }, camel_case = function(string) { return string.replace( rmsPrefix, "ms-" ).replace(rdashAlpha, fcamelCase); }; // ConstraintJS Core Functionality // ------------------------------- var Constraint, // Declare here, will be defined later ArrayConstraint, MapConstraint, is_constraint, is_array, is_map, // Save the previous value of the `cjs` variable. old_cjs = root.cjs, /** * `cjs` is ConstraintJS's only *visible* object; every other method an property is a property of `cjs`. * The `cjs` object itself can also be called to create a constraint object. * * @method cjs * @param {object} value - A map of initial values * @param {object} options - A set of options to control how the array constraint is evaluated * @return {cjs.ArrayConstraint} A new array constraint * @see cjs.noConflict * * @example Creating an array constraint * * var cjs_arr = cjs([1,2,3]); * cjs_arr.item(0); // 1 */ /** * Input value constraint * * @method cjs^2 * @param {dom} node - The DOM node whose value to follow * @return {cjs.Binding} A constraint whose value is the current value of the input * * @example Creating an input value constraint * * var inp_elem = document.getElementById('myTextInput'), * cjs_val = cjs(inp_elem); */ /** * Create a map constraint * * @method cjs^3 * @param {object} value - A map of initial values * @param {object} options - A set of options to control how the map constraint is evaluated * @return {cjs.MapConstraint} A new map constraint * * @example Creating a map constraint * * var cobj_obj = cjs({ * foo: 1 * }); * cobj.get('foo'); // 1 * cobj.put('bar', 2); * cobj.get('bar') // 2 */ /** * Create a standard constraint * * @method cjs^4 * @param {object} value - The constraint's value * @param {object} options - A set of options to control how the constraint is evaluated * @return {cjs.Constraint} A new constraint * * @example Creating an empty constraint * * var x = cjs(), * y = cjs(1), * z = cjs(function() { * return y.get() + 1; * }); * x.get(); // undefined * y.get(); // 1 * z.get(); // 2 * * @example With options * * var yes_lit = cjs(function() { return 1; }, * { literal: true }), * var not_lit = cjs(function() { return 1; }, * { literal: false }); * yes_lit.get(); // (function) * not_lit.get(); // 1 */ cjs = function (arg0, arg1) { if(isArray(arg0)) { return new ArrayConstraint(extend({ value: arg0 }, arg1)); } else if(isPolyDOM(arg0)) { return cjs.inputValue(arg0); } else if(is_constraint(arg0)) { return new Constraint(arg0, arg1); } else if(isObject(arg0) && !isFunction(arg0)) { return new MapConstraint(extend({ value: arg0 }, arg1)); } else { return new Constraint(arg0, arg1); } }, get_constraint_val = function(x) { return is_constraint(x) ? x.get() : x; }; // Constraint Solver // ----------------- // Implements constraint solving, as described in: // [Integrating pointer variables into one-way constraint models](http://doi.acm.org/10.1145/180171.180174) // Edge from A -> B means A sends data to B var constraint_solver = { // `stack` keeps track of the list of constraints that is currently being fetched. If constraint A is fetched while B is on the top of the stack // then A is added to the top of the stack and B is marked as dependent on A stack: [], check_on_nullified_ids: {}, // node is the Constraint whose value we are fetching and auto_add_outgoing specifies whether dependencies FROM node should // be automatically added getValue: function (auto_add_outgoing, getter_arg) { var node = this, stack = constraint_solver.stack, stack_len = stack.length, demanding_var, dependency_edge, tstamp; if (stack_len > 0) { // There's a constraint that's asking for my value // Let's call it demanding_var demanding_var = stack[stack_len - 1]; dependency_edge = node._outEdges[demanding_var._id]; tstamp = demanding_var._tstamp+1; // If there's already a dependency set up, mark it as still being used by setting its timestamp to the demanding // variable's timestamp + `1` (because that variable's timestamp will be incrememted later on, so they will be equal) // // Code in the this.nullify will check this timestamp and remove the dependency if it's out of date if(dependency_edge) { // Update timestamp dependency_edge.tstamp = tstamp; } else if(node !== demanding_var) { // Make sure that the dependency should be added if (node._options.auto_add_outgoing_dependencies !== false && demanding_var._options.auto_add_incoming_dependencies !== false && auto_add_outgoing !== false) { // and add it if it should node._outEdges[demanding_var._id] = demanding_var._inEdges[node._id] = {from: node, to: demanding_var, tstamp: tstamp}; } } } // This node is waiting for an asyncronous value if(node._paused_info) { // So return its temporary value until then return node._paused_info.temporaryValue; } else if (!node._valid) { // If the node's cached value is invalid... // Set the timestamp before fetching in case a constraint depends on itself node._tstamp++; // Push node onto the stack to make it clear that it's being fetched stack[stack_len] = node; // Mark it as valid node._valid = true; if (node._options.cache_value !== false) { // Check if dynamic value. If it is, then call it. If not, just fetch it // set this to the node's cached value, which will be returned node._cached_value = node._options.literal ? node._value : (isFunction(node._value) ? node._value.call(node._options.context || node, node, getter_arg) : get_constraint_val(node._value)); // The node paused as if this was going to be an asyncronous value but it ended up being syncronous. // Use that to set the value if(node._sync_value) { node._cached_value = node._sync_value.value; delete node._sync_value; } else if(constraint_solver._paused_node && constraint_solver._paused_node.node === node) { // The node said it would have an asyncronous value and it did // Save the paused information to the node and remove it from the constraint solver node._paused_info = constraint_solver._paused_node; delete constraint_solver._paused_node; //Restore the stack to avoid adding a self-dependency stack.length = stack_len; // And return the temporary value return node._paused_info.temporaryValue; } } else if(isFunction(node._value)) { // if it's just a non-cached function call, just call the function node._value.call(node._options.context); } // Pop the item off the stack stack.length = stack_len; } return node._cached_value; }, // Called when a constraint's getter is paused pauseNodeGetter: function(temporaryValue) { constraint_solver._paused_node = { temporaryValue: temporaryValue, node: this }; }, // Called when a constraint's getter is resumed resumeNodeGetter: function(value) { var node = this, old_stack; // Hey! The node said its value would be asyncronous but it ended up being syncronous // We know because, it paused and then resumed before the constraint solver's paused node information could even // be removed. if(constraint_solver._paused_node && constraint_solver._paused_node.node === node) { delete constraint_solver._paused_node; node._sync_value = { value: value }; } else { // Nullify every dependent node and update this node's cached value old_stack = constraint_solver.stack; delete node._paused_info; node._tstamp++; node._valid = true; constraint_solver.stack = [node]; if (node._options.cache_value !== false) { // Check if dynamic value. If it is, then call it. If not, just fetch it // set this to the node's cached value, which will be returned node._cached_value = node._options.literal ? value : (isFunction(value) ? value.call(node._options.context || node, node) : cjs.get(value)); } else if(isFunction(node._value)) { // if it's just a non-cached function call, just call the function value.call(node._options.context); } constraint_solver.nullify.apply(constraint_solver, map(node._outEdges, function(edge) { return edge.to; })); constraint_solver.stack = old_stack; } }, // Utility function to mark a listener as being in the call stack. `this` refers to the constraint node here add_in_call_stack: function(nl) { var nl_priority = nl.priority; nl.in_call_stack++; nl.node._num_listeners_in_call_stack++; if(isNumber(nl_priority)) { var i = 0, len = this.nullified_call_stack.length, item, item_priority; while(i < len) { item = this.nullified_call_stack[i]; if(item) { item_priority = item.priority; if(item_priority === false || item_priority < nl_priority) { this.nullified_call_stack.splice(i, 0, nl); return; } } i++; } } this.nullified_call_stack.push(nl); }, nullify: function() { // Unfortunately, running nullification listeners can, in some cases cause nullify to be indirectly called by itself // (as in while running `nullify`). The variable is_root will prevent another call to `run_nullification_listeners` at // the bottom of this function var i, outgoingEdges, toNodeID, invalid, curr_node, equals, old_value, new_value, changeListeners, to_nullify = slice.call(arguments), to_nullify_len = to_nullify.length, is_root = !this._is_nullifying,curr_node_id; if (is_root) { // This variable is used to track `is_root` for any potential future calls this._is_nullifying = true; } // Using a list instead of a recursive call because the call stack can get tall and annoying for debugging with // recursive calls for (i = 0; i < to_nullify_len; i+= 1) { curr_node = to_nullify[i]; // the constraint we are currently nullifying to_nullify[i] = false; // To save space, stop keeping track of the object (also useful for debugging occasionally) // We only care to nullify if the current node is actually valid if (curr_node._valid) { curr_node._valid = false; // Mark it as invalid... invalid = true; // The user can also optionally check if the node should be nullified. This is useful if a large number of nodes // depend on this node, and the potential cost of nullifying/re-evaluating them is higher than the cost of // re-evaluating this node if (curr_node._options.cache_value !== false && curr_node._options.check_on_nullify === true && // check to make sure we aren't already getting this node to avoid an infinite loop !this.check_on_nullified_ids[curr_node._id]) { this.check_on_nullified_ids[curr_node._id] = true; // Only mark as invalid if the old value is different from the current value. equals = curr_node._options.equals || eqeqeq; old_value = curr_node._cached_value; new_value = curr_node.get(undefined, true); if (equals(old_value, new_value)) { invalid = false; } } // If I'm still invalid, after a potential check if (invalid) { // Add all of the change listeners to the call stack, and mark each change listener // as being in the call stack changeListeners = curr_node._changeListeners; each(changeListeners, this.add_in_call_stack, this); // Then, get every outgoing edge and add it to the nullify queue outgoingEdges = curr_node._outEdges; curr_node_id = curr_node._id; for (toNodeID in outgoingEdges) { if (has(outgoingEdges, toNodeID)) { var outgoingEdge = outgoingEdges[toNodeID]; var dependentNode = outgoingEdge.to; // If the edge's timestamp is out of date, then this dependency isn't used // any more and remove it if (outgoingEdge.tstamp < dependentNode._tstamp) { delete curr_node._outEdges[toNodeID]; delete dependentNode._inEdges[curr_node_id]; } else { // But if the dependency still is being used, then add it to the nullification // queue to_nullify[to_nullify_len] = dependentNode; to_nullify_len += 1; } } } } } } // If I'm the first one, then run the nullification listeners and remove the is_nullifying flag if (is_root) { this.check_on_nullified_ids = {}; // If nobody told us to wait, then run the nullification listeners if (this.semaphore >= 0 && this.nullified_call_stack.length > 0) { this.run_nullified_listeners(); } delete this._is_nullifying; } }, /** * * Remove the edge going from `fromNode` to `toNode` * @method cjs.removeDependency */ removeDependency: function(fromNode, toNode) { delete fromNode._outEdges[toNode._id]; delete toNode._inEdges[fromNode._id]; }, // Use a semaphore to decide when running the nullification listeners is appropriate semaphore: 0, /** * Tells the constraint solver to delay before running any `onChange` listeners * * Note that `signal` needs to be called the same number of times as `wait` before * the `onChange` listeners will run. * @method cjs.wait * @see cjs.signal * @see cjs.onChange * @example * var x = cjs(1); * x.onChange(function() { * console.log('x changed'); * }); * cjs.wait(); * x.set(2); * x.set(3); * cjs.signal(); // output: x changed */ wait: function() { this.semaphore -= 1; }, /** * Tells the constraint solver it is ready to run any `onChange` listeners. * Note that `signal` needs to be called the same number of times as `wait` before * the `onChange` listeners will run. * @method cjs.signal * @see cjs.wait * @see cjs.onChange * @example * var x = cjs(1); * x.onChange(function() { * console.log('x changed'); * }); * cjs.wait(); * cjs.wait(); * x.set(2); * x.set(3); * cjs.signal(); * cjs.signal(); // output: x changed */ signal: function () { this.semaphore += 1; // When we signal that we're ready, try running the call stack if (this.semaphore >= 0 && this.nullified_call_stack.length > 0) { this.run_nullified_listeners(); } }, // The list of nullified listeners to run nullified_call_stack: [], // Tracks whether we are in the middle of running the nullification listeners running_listeners: false, // Clear all of the dependencies clearEdges: function(node, silent) { var loud = silent !== true, node_id = node._id, edge, key, inEdges = node._inEdges, outEdges = node._outEdges; if(loud) { this.wait(); } // Clear the incoming edges for(key in inEdges) { if(has(inEdges, key)) { delete inEdges[key].from._outEdges[node_id]; delete inEdges[key]; } } // and the outgoing edges for(key in outEdges) { if(has(outEdges, key)) { var toNode = outEdges[key].to; if (loud) { constraint_solver.nullify(toNode); } delete toNode._inEdges[node_id]; delete outEdges[key]; } } if(loud) { this.signal(); } }, run_nullified_listeners: function () { var nullified_info, callback, context; // Make sure `run_nullified_listeners` isn't indirectly called by itself if (!this.running_listeners) { this.running_listeners = true; while (this.nullified_call_stack.length > 0) { nullified_info = this.nullified_call_stack.shift(); callback = nullified_info.callback; context = nullified_info.context || root; nullified_info.in_call_stack--; nullified_info.node._num_listeners_in_call_stack--; // If in debugging mode, then call the callback outside of a `try` statement if(cjs.__debug) { callback.apply(context, nullified_info.args); } else { try { // Call the nullification callback with any specified arguments callback.apply(context, nullified_info.args); } catch(e) { if(has(root, "console")) { root.console.error(e); } } } } this.running_listeners = false; } }, remove_from_call_stack: function(info) { while(info.in_call_stack > 0) { remove(this.nullified_call_stack, info); info.in_call_stack--; info.node._num_listeners_in_call_stack--; } } }; // Constraint Variables // -------------------- /** * ***Note***: The preferred way to create a constraint is with the `cjs.constraint` function (lower-case 'c') * `cjs.Constraint` is the constructor for the base constraint. Valid properties for `options` are: * * - `auto_add_outgoing_dependencies`: allow the constraint solver to determine when things depend on me. *default:* `true` * - `auto_add_incoming_dependencies`: allow the constraint solver to determine when things I depend on things. *default:* `true` * - `cache_value`: whether or not to keep track of the current value. *default:* `true` * - `check_on_nullify`: when nullified, check if my value has actually changed (requires immediately re-evaluating me). *default*: `false` * - `context`: if `value` is a function, the value of `this`, when that function is called. *default:* `window` * - `equals`: the function to check if two values are equal, *default:* `===` * - `literal`: if `value` is a function, the value of the constraint should be the function itself (not its return value). *default:* `false` * - `run_on_add_listener`: when `onChange` is called, whether or not immediately validate the value. *default:* `true` * * @class cjs.Constraint * @classdesc A constraint object communicates with the constraint solver to store and maintain constraint values * @param {*} value - The initial value of the constraint or a function to compute its value * @param {Object} [options] - A set of options to control how and when the constraint's value is evaluated: */ Constraint = function (value, options) { // These are all hidden values that should not be referred to directly this._options = extend({ context: root }, options); // keeps track of the above options this._value = value; // Constant or a function this._id = uniqueId(); // different for every constraint, helps with optimizing speed this._outEdges = {}; // The nodes that depend on me, key is link to edge object (with properties toNode, fromNode=this) this._inEdges = {}; // The nodes that I depend on, key is link to edge object (with properties toNode=this, fromNode) this._changeListeners = []; // A list of callbacks that will be called when I'm nullified this._tstamp = 0; // Marks the last time I was updated this._num_listeners_in_call_stack = 0; // the number of listeners that are in the call stack if(this._options.literal || (!isFunction(this._value) && !is_constraint(this._value))) { // We already have a value that doesn't need to be computed this._valid = true; // Tracks whether or not the cached value if valid this._cached_value = this._value; // Caches the node's value } else { this._valid = false; this._cached_value = undefined; } }; (function(My) { var proto = My.prototype; /** @lends cjs.Constraint.prototype */ /** * Get the current value of this constraint. For computed constraints, if the constraint is invalid, its value will be re-computed. * * * @method get * @param {boolean} [autoAddOutgoing=true] - Whether to automatically add a dependency from this constraint to ones that depend on it. * @return {*} The current constraint value * @see set * * @example * var x = cjs(1); * x.get(); // 1 */ proto.get = constraint_solver.getValue; /** * Change the current value of the constraint. Other constraints that depend on its value will be invalidated. * * @method set * @see cjs.Constraint * @param {*} value - The initial value of the constraint or a function to compute its value * @param {Object} [options] - A set of options to control how and when the constraint's value is evaluated: * @return {cjs.Constraint} - `this` * @see get * @see invalidate * * @example * var x = cjs(1); * x.get(); // 1 * x.set(function() { return 2; }); * x.get(); // 2 * x.set('c'); * x.get(); // 'c' */ proto.set = function (value, options) { var old_value = this._value; this._value = value; if(options && options.silent === true) { return this; } else if (this._options.literal || (!isFunction(value) && !is_constraint(value))) { // If it's a value // Then use the specified equality check var equality_check = this._options.equal || eqeqeq; if(!equality_check(old_value, value)) { // And nullify if they aren't equal constraint_solver.nullify(this); } } else if(old_value !== value) { // Otherwise, check function equality // And if they aren't the same function, nullify constraint_solver.nullify(this); } return this; }; /** * Change how this constraint is computed (see Constraint options) * * @method setOption * @see cjs.Constraint * @param {Object} options - An object with the options to change * @return {cjs.Constraint} - `this` * * @example * var x = cjs(function() { return 1; }); * x.get(); // 1 * x.setOption({ * literal: true, * auto_add_outgoing_dependencies: false * }); * x.get(); // (function) */ /** * @method setOption^2 * @see cjs.Constraint * @param {String} key - The name of the option to set * @param {*} value - The option's new value * @return {cjs.Constraint} - `this` * * @example * var x = cjs(function() { return 1; }); * x.get(); // 1 * x.setOption("literal", true); * x.get(); // (function) */ var invalidation_arguments = ["context", "literal"]; proto.setOption = function(arg0, arg1) { var to_invalidate; if(isString(arg0)) { this._options[arg0] = arg1; to_invalidate = indexOf(invalidation_arguments, arg0) >= 0; } else { var keys = keys(arg0); extend(this._options, arg0); to_invalidate = any(invalidation_arguments, function(ia) { return keys.indexOf(ia) >= 0; }); } // Nullify my value regardless of what changed // changing context, literal, etc. might change my value return to_invalidate ? this.invalidate() : this; }; /** * Mark this constraint's value as invalid. This signals that the next time its value is fetched, * it should be recomputed, rather than returning the cached value. * * An invalid constraint's value is only updated when it is next requested (for example, via `.get()`). * * @method invalidate * @return {cjs.Constraint} - `this` * @see isValid * * @example Tracking the window height * var height = cjs(window.innerHeight); * window.addEventListener("resize", function() { * height.invalidate(); * }); */ proto.invalidate = function () { constraint_solver.nullify(this); return this; }; /** * Find out if this constraint's value needs to be recomputed (i.e. whether it's invalid). * * An invalid constraint's value is only updated when it is next requested (for example, via `.get()`). * * @method isValid * @return {boolean} - `true` if this constraint's current value is valid. `false` otherwise. * @see invalidate * * @example * var x = cjs(1), * y = x.add(2); * y.get(); // 3 * y.isValid(); // true * x.set(2); * y.isValid(); // false * y.get(); // 4 * y.isValid(); //true */ proto.isValid = function () { return this._valid; }; /** * Removes every dependency to this node * * @method remove * @param {boolean} [silent=false] - If set to `true`, avoids invalidating any dependent constraints. * @return {cjs.Constraint} - `this` * @see destroy */ proto.remove = function (silent) { constraint_solver.clearEdges(this, silent); this._valid = false; // In case it gets used in the future, make sure this constraint is marked as invalid this._cached_value = undefined; // and remove the cached value return this; }; /** * Removes any dependent constraint, clears this constraints options, and removes every change listener. This is * useful for making sure no memory is deallocated * * @method destroy * @param {boolean} [silent=false] - If set to `true`, avoids invalidating any dependent constraints. * @return {cjs.Constraint} - `this` * @see remove * * @example * var x = cjs(1); * x.destroy(); // ...x is no longer needed */ proto.destroy = function (silent) { if(this._num_listeners_in_call_stack > 0) { each(this._changeListeners, function(cl) { // remove it from the call stack if (cl.in_call_stack>0) { constraint_solver.remove_from_call_stack(cl); if(this._num_listeners_in_call_stack === 0) { return breaker; } } }, this); } this.remove(silent); this._changeListeners = []; return this; }; /** * Signal that this constraint's value will be computed later. For instance, for asyncronous values. * * @method pauseGetter * @param {*} temporaryValue - The temporary value to use for this node until it is resumed * @return {cjs.Constraint} - `this` * @see resumeGetter */ proto.pauseGetter = function () { constraint_solver.pauseNodeGetter.apply(this, arguments); return this; }; /** * Signal that this Constraint, which has been paused with `pauseGetter` now has a value. * * @method resumeGetter * @param {*} value - This node's value * @return {cjs.Constraint} - `this` * @see pauseGetter * */ proto.resumeGetter = function () { constraint_solver.resumeNodeGetter.apply(this, arguments); return this; }; /** * Call `callback` as soon as this constraint's value is invalidated. Note that if the constraint's value * is invalidated multiple times, `callback` is only called once. * * @method onChange * @param {function} callback * @param {*} [thisArg=window] - The context to use for `callback` * @param {*} ...args - The first `args.length` arguments to `callback` * @return {cjs.Constraint} - `this` * @see offChange * * @example * var x = cjs(1); * x.onChange(function() { * console.log("x is " + x.get()); * }); * x.set(2); // x is 2 */ proto.onChange = function(callback, thisArg) { return this.onChangeWithPriority.apply(this, ([false]).concat(toArray(arguments))); }; proto.onChangeWithPriority = function(priority, callback, thisArg) { var args = slice.call(arguments, 3); // Additional arguments if(!isNumber(priority)) { priority = false; } this._changeListeners.push({ callback: callback, // function context: thisArg, // 'this' when called args: args, // arguments to pass into the callback in_call_stack: 0, // internally keeps track of if this function will be called in the near future node: this, priority: priority }); if(this._options.run_on_add_listener !== false) { // Make sure my current value is up to date but don't add outgoing constraints. // That way, when it changes the callback will be called this.get(false); } return this; }; /** * Removes the first listener to `callback` that was created by `onChange`. `thisArg` is optional and * if specified, it only removes listeners within the same context. If thisArg is not specified, * the first `callback` is removed. * * @method offChange * @param {function} callback * @param {*} [thisArg] - If specified, only remove listeners that were added with this context * @return {cjs.Constraint} - `this` * @see onChange * * var x = cjs(1), * callback = function(){}; * x.onChange(callback); * // ... * x.offChange(callback); */ proto.offChange = function (callback, thisArg) { var cl, i; for(i = this._changeListeners.length-1; i>=0; i-=1) { cl = this._changeListeners[i]; // Same callback and either the same context or context wasn't specified if(cl.callback === callback && (!thisArg || cl.context === thisArg)) { // Then get rid of it removeIndex(this._changeListeners, i); // And remove it if it's in the callback if (cl.in_call_stack>0) { constraint_solver.remove_from_call_stack(cl); } delete cl.node; // Only searching for the last one break; } } return this; }; /** * Change this constraint's value in different states * * @method inFSM * @param {cjs.FSM} fsm - The finite-state machine to depend on * @param {Object} values - Keys are the state specifications for the FSM, values are the value for those specific states * @return {cjs.Constraint} - `this` * * @example * var fsm = cjs.fsm("state1", "state2") * .addTransition("state1", "state2", * cjs.on("click")); * var x = cjs().inFSM(fsm, { * state1: 'val1', * state2: function() { return 'val2'; } * }); */ proto.inFSM = function(fsm, values) { each(values, function(v, k) { // add listeners to the fsm for that state that will set my getter's value fsm.on(k, function() { this.set(v); }, this); if(fsm.is(k)) { this.set(v); } }, this); return this; }; /** * Returns the last value in the array `[this].concat(args)` if every value is truthy. Otherwise, returns `false`. * Every argument won't necessarily be evaluated. For instance: * * - `x = cjs(false); cjs.get(x.and(a))` does not evaluate `a` * * @method and * @param {*} ...args - Any number of constraints or values to pass the "and" test * @return {cjs.Constraint|*} - A constraint whose value is `false` if this or any passed in value is falsy. Otherwise, the last value passed in. * * @example * * var x = c1.and(c2, c3, true); */ proto.and = function() { var args = ([this]).concat(toArray(arguments)), len = args.length; return new My(function() { var i = 0, val; for(;i` element `inp_elem` * * var inp_val = cjs(inp_elem).toInt(); */ proto.toInt = createConstraintModifier(function(me) { return parseInt.apply(this, arguments); }); /** * Float conversion constraint modifier. * @method toFloat * @return {*} - A constraint whose value is parseFloat(this) * @example Given `` element `inp_elem` * * var inp_val = cjs(inp_elem).toFloat(); */ proto.toFloat = createConstraintModifier(function(me) { return parseFloat.apply(this, arguments); }); // For all the arithmetic operators, allow any number of arguments to be passed in. For example: /** * Addition constraint modifier * @method add * @param {number} ...args - Any number of constraints or numbers * @return {cjs.Constraint} - A constraint whose value is `this.get() + args[0].get() + args[1].get() + ...` * @example * * x = y.add(1,2,z); // x <- y + 1 + 2 + z * @example The same method can also be used to add units to values * * x = y.add("px"); // x <- ypx */ proto.add = createConstraintModifier(function() { return reduce(arguments, binary_operators["+"], 0); }); /** * Subtraction constraint modifier * @method sub * @param {number} ...args - Any number of constraints or numbers * @return {cjs.Constraint} - A constraint whose value is `this.get() - args[0].get() - args[1].get() - ...` * @example * * x = y.sub(1,2,z); // x <- y - 1 - 2 - z */ proto.sub = createConstraintModifier(function(me) { return reduce(rest(arguments), binary_operators["-"], me); }); /** * Multiplication constraint modifier * @method mul * @param {number} ...args - Any number of constraints or numbers * @return {number} - A constraint whose value is `this.get() * args[0].get() * args[1].get() * ...` * @example * * x = y.mul(1,2,z); //x <- y * 1 * 2 * z */ proto.mul = createConstraintModifier(function(me) { return reduce(rest(arguments), binary_operators["*"], me); }); /** * Division constraint modifier * @method div * @param {number} ...args - Any number of constraints or numbers * @return {number} - A constraint whose value is `this.get() / args[0].get() / args[1].get() / ...` * @example * * x = y.div(1,2,z); // x <- y / 1 / 2 / z */ proto.div = createConstraintModifier(function(me) { return reduce(rest(arguments), binary_operators["/"], me); }); /** * Absolute value constraint modifier * @method abs * @return {number} - A constraint whose value is `Math.abs(this.get())` * @example * * x = c1.abs(); // x <- abs(c1) */ /** * Floor * @method floor * @return {number} - A constraint whose value is `Math.floor(this.get())` * @example * * x = c1.floor(); // x <- floor(c1) */ /** * Ceil * @method ceil * @return {number} - A constraint whose value is `Math.ceil(this.get())` * @example * * x = c1.ceil(); // x <- ceil(c1) */ /** * Round * @method round * @return {number} - A constraint whose value is `Math.round(this.get())` * @example * * x = c1.round(); // x <- round(c1) */ /** * Square root * @method sqrt * @return {number} - A constraint whose value is `Math.sqrt(this.get())` * @example * * x = c1.sqrt(); // x <- sqrt(c1) */ /** * Arccosine * @method acos * @return {number} - A constraint whose value is `Math.acos(this.get())` * @example * * angle = r.div(x).acos() */ /** * Arcsin * @method asin * @return {number} - A constraint whose value is `Math.asin(this.get())` * @example * * angle = r.div(y).asin() */ /** * Arctan * @method atan * @return {number} - A constraint whose value is `Math.atan(this.get())` * @example * * angle = y.div(x).atan() */ /** * Arctan2 * @method atan2 * @param {number|cjs.Constraint} x * @return {number} - A constraint whose value is `Math.atan2(this.get()/x.get())` * @example * * angle = y.atan2(x) */ /** * Cosine * @method cos * @return {number} - A constraint whose value is `Math.cos(this.get())` * @example * * dx = r.mul(angle.cos()) */ /** * Sine * @method sin * @return {number} - A constraint whose value is `Math.sin(this.get())` * @example * * dy = r.mul(angle.sin()) */ /** * Tangent * @method tan * @return {number} - A constraint whose value is `Math.tan(this.get())` * @example * * dy = r.mul(angle.sin()) */ /** * Max * @method max * @param {number} ...args - Any number of constraints or numbers * @return {number} - A constraint whose value is the **highest** of `this.get()`, `args[0].get()`, `args[1].get()`... * @example * * val = val1.max(val2, val3); */ /** * Min * @method min * @param {number} ...args - Any number of constraints or numbers * @return {number} - A constraint whose value is the **lowest** of `this.get()`, `args[0].get()`, `args[1].get()`... * @example * * val = val1.min(val2, val3); */ /** * Power * @method pow * @param {number} x - The exponent * @return {number} - A constraint whose value is `Math.pow(this.get(), x.get())` * @example * * d = dx.pow(2).add(dy.pow(2)).sqrt() */ /** * Natural Log (base e) * @method log * @return {number} - A constraint whose value is `Math.log(this.get())` * @example * * num_digits = num.max(2).log().div(Math.log(10)).ceil() */ /** * Exp (E^x) * @method exp * @return {number} - A constraint whose value is `Math.exp(this.get())` * @example * * neg_1 = cjs(i*pi).exp() */ each(["abs", "acos", "asin", "atan", "atan2", "cos", "max", "min", "sin", "tan", "pow", "round", "floor", "ceil", "sqrt", "log", "exp"], function(op_name) { proto[op_name] = createConstraintModifier(bind(Math[op_name], Math)); }); /** * Coerce an object to a number * @method pos * @return {number} - A constraint whose value is `+(this.get())` * @example * * numeric_val = val.pos() */ /** * Negative operator * @method neg * @return {number} - A constraint whose value is `-(this.get())` * @example * * neg_val = x.neg() */ /** * Not operator * @method not * @return {boolean} - A constraint whose value is `!(this.get())` * @example * * opposite = x.not() */ /** * Bitwise not operator * @method bitwiseNot * @return {number} - A constraint whose value is `~(this.get())` * @example * * inverseBits = val.bitwiseNot() */ /** * Equals unary operator * @method eq * @param {*} other - A constraint or value to compare against * @return {boolean} - A constraint whose value is `this.get() == other.get()` * * @example * * isNull = val.eq(null) */ /** * Not equals operator * @method neq * @param {*} other - A constraint or value to compare against * @return {boolean} - A constraint whose value is `this.get() != other.get()` * * @example * * notNull = val.neq(null) */ /** * Strict equals operator * @method eqStrict * @param {*} other - A constraint or value to compare against * @return {boolean} - A constraint whose value is `this.get() === other.get()` * * @example * * isOne = val.eqStrict(1) */ /** * Not strict equals binary operator * @method neqStrict * @param {*} other - A constraint or value to compare against * @return {boolean} - A constraint whose value is `this.get() !== other.get()` * * @example * * notOne = val.neqStrict(1) */ /** * @method gt * @param {*} other - A constraint or value to compare against * @return {boolean} - A constraint whose value is `this.get() > other.get()` * * @example * * isPositive = val.gt(0) */ /** * @method lt * @param {*} other - A constraint or value to compare against * @return {boolean} - A constraint whose value is `this.get() < other.get()` * * @example * * isNegative = val.lt(0) */ /** * @method ge * @param {*} other - A constraint or value to compare against * @return {boolean} - A constraint whose value is `this.get() >= other.get()` * * @example * * isBig = val.ge(100) */ /** * @method le * @param {*} other - A constraint or value to compare against * @return {boolean} - A constraint whose value is `this.get() <= other.get()` * * @example * * isSmall = val.le(100) */ /** * @method xor * @param {*} other - A constraint or value to compare against * @return {number} - A constraint whose value is `this.get() ^ other.get()` */ /** * @method bitwiseAnd * @param {*} other - A constraint or value to compare against * @return {number} - A constraint whose value is `this.get() & other.get()` */ /** * @method bitwiseOr * @param {*} other - A constraint or value to compare against * @return {number} - A constraint whose value is `this.get() | other.get()` */ /** * @method mod * @param {*} other - A constraint or value to compare against * @return {number} - A constraint whose value is `this.get() % other.get()` * @example * isEven = x.mod(2).eq(0); * */ /** * @method rightShift * @param {*} other - A constraint or value to compare against * @return {number} - A constraint whose value is `this.get() >> other.get()` */ /** * @method leftShift * @param {*} other - A constraint or value to compare against * @return {number} - A constraint whose value is `this.get() << other.get()` */ /** * @method unsignedRightShift * @param {*} other - A constraint or value to compare against * @return {number} - A constraint whose value is `this.get() >>> other.get()` */ each({ u: { // Unary operators pos: "+", neg: "-", not: "!", bitwiseNot: "~" }, bi: { // Binary operators eqStrict: "===",neqStrict: "!==", eq: "==",neq: "!=", gt: ">", ge: ">=", lt: "<", le: "<=", xor: "^", bitwiseAnd: "&", bitwiseOr: "|", mod: "%", rightShift:">>",leftShift: "<<", unsignedRightShift: ">>>" } }, function(ops, operator_prefix) { var op_list = operator_prefix === "u" ? unary_operators : binary_operators; each(ops, function(key, op_name) { proto[op_name] = createConstraintModifier(op_list[key]); }); }); /** * Object type modifier * @method typeOf * @return {*} - a constraint whose value is `typeof this.get()` * @example * * var valIsNumber = val.typeOf().eq('[object Number]') */ proto.typeOf = createConstraintModifier(function(a) { return typeof a;}); /** * Object instance check modifier * @method instanceOf * @param {*} other - a constraint or value to compare against * @return {boolean} - a constraint whose value is `this.get() instanceof other.get()` * @example * * var valIsArray = val.instanceof(Array) */ proto.instanceOf = createConstraintModifier(function(a, b) { return a instanceof b;}); } (Constraint)); /** @lends */ // Create some exposed utility functions /** * Determine whether an object is a constraint * @method cjs.isConstraint * @param {*} obj - An object to check * @return {boolean} - `obj instanceof cjs.Constraint` */ is_constraint = function(obj) { return obj instanceof Constraint; }; // Expore core functions // ------------------------- extend(cjs, { /** * Constraint constructor * * @method cjs.constraint * @constructs cjs.Constraint * @param {*} value - The initial value of the constraint or a function to compute its value * @param {Object} [options] - A set of options to control how and when the constraint's value is evaluated * @return {cjs.Constraint} - A new constraint object * @see cjs.Constraint */ constraint: function(value, options) { return new Constraint(value, options); }, /** @expose cjs.Constraint */ Constraint: Constraint, /** @expose cjs.isConstraint */ isConstraint: is_constraint, /** * Create a new constraint whose value changes by state * * @method cjs.inFSM * @param {cjs.FSM} fsm - The finite-state machine to depend on * @param {Object} values - Keys are the state specifications for the FSM, values are the value for those specific states * @return {cjs.Constraint} - A new constraint object * @see cjs.Constraint.prototype.inFSM * * @example * * var fsm = cjs.fsm("state1", "state2") * .addTransition("state1", "state2", * cjs.on("click")); * var x = cjs.inFSM(fsm, { * state1: 'val1', * state2: function() { return 'val2'; } * }); */ inFSM: function(fsm, values) { return (new Constraint()).inFSM(fsm, values); }, /** * Gets the value of an object regardless of if it's a constraint (standard, array, or map) or not. * * @method cjs.get * @param {*} obj - The object whose value to return * @param {boolean} [autoAddOutgoing=true] - Whether to automatically add a dependency from this constraint to ones that depend on it. * @return {*} - The value * * @see cjs.isConstraint * @see cjs.Constraint.prototype.get * @see cjs.isArrayConstraint * @see cjs.ArrayConstraint.prototype.toArray * @see cjs.isMapConstraint * @see cjs.MapConstraint.prototype.toObject * * @example * var w = 1, * x = cjs(2), * y = cjs(['a','b']), * z = cjs({c: 2}); * * cjs.get(w); // 1 * cjs.get(x); // 2 * cjs.get(y); // ['a','b'] * cjs.get(z); // {c: 2} */ get: function (obj, arg0) { if(is_constraint(obj)) { return obj.get(arg0); } else if(is_array(obj)) { return obj.toArray(); } else if(is_map(obj)) { return obj.toObject(); } else { return obj; } }, /** @expose cjs.wait */ wait: bind(constraint_solver.wait, constraint_solver), /** @expose cjs.signal */ signal: bind(constraint_solver.signal, constraint_solver), /** @expose cjs.removeDependency */ removeDependency: constraint_solver.removeDependency, /** @expose cjs.arrayDiff */ arrayDiff: get_array_diff, // expose this useful function /** * The version number of ConstraintJS * @property {string} cjs.version * @see cjs.toString */ version: "0.9.8-beta2", // This template will be filled in by the builder /** * Print out the name and version of ConstraintJS * * @method cjs.toString * @return {string} - `ConstraintJS v(version#)` * @see cjs.version */ toString: function() { return "ConstraintJS v" + cjs.version; }, /** @private */ __debug: true, /** * Restore the previous value of `cjs` * * @method cjs.noConflict * @return {object} - `cjs` * * @example Renaming `cjs` to `ninjaCJS` * * var ninjaCJS = cjs.noConflict(); * var x = ninjaCJS(1); * @see cjs */ noConflict: has(root, "cjs") ? function() { // If there was a previous `cjs` property then track it // and allow `cjs.noConflict` to restore its previous value if(root.cjs === cjs) { root.cjs = old_cjs; } // and return a reference to `cjs` if the user wants it return cjs; } : // ...otherwise, `cjs.noConflict` will just delete the old value function() { delete root.cjs; return cjs; } }); // Array Constraints // ----------------- var isPositiveInteger = function (val) { return isNumber(val) && Math.round(val) === val && val >= 0; }; /** * ***Note:*** The preferred constructor for arrays is `cjs.array` * * This class is meant to emulate standard arrays, but with constraints * It contains many of the standard array functions (push, pop, slice, etc) * and makes them constraint-enabled. * * x[1] = y[2] + z[3] === x.item(1, y.item(2) + z.item(3)) * * Options: * * - `equals`: the function to check if two values are equal, *default:* `===` * - `value`: an array for the initial value of this constraint * * @class cjs.ArrayConstraint * @classdesc A class that adds constraint to arrays * @param {Object} [options] - A set of options to control how the array constraint is evaluated * * @see cjs * @see cjs.array */ ArrayConstraint = function (options) { options = extend({ equals: eqeqeq, // How to check for equality, useful for indexOf, etc value: [] // starting value }, options); // Every value in the array is a constraint this._value = map(options.value, function(val) { return new Constraint(val, {literal: true}); }); // When we fetch an item in the array that doesn't exist, it gets added to // the unsubstantiated items list to create a dependency this._unsubstantiated_items = []; this.$len = new Constraint(this._value.length); // Keep track of the array length in a constraint this.$equality_check = new Constraint(options.equals, {literal: true}); // How to check for equality again... }; (function (my) { var proto = my.prototype; /** * Any iterator in forEach can return this object to break out of its loop. * @property {object} cjs.ArrayConstraint.BREAK */ my.BREAK = {}; /** @lends cjs.ArrayConstraint.prototype */ // Get a particular item in the array var _get = function (arr, key) { var val = arr._value[key]; if (val === undefined) { // Even if arr[key] is set to undefined, it would be a constraint // Create a dependency so that if the value for this key changes // later on, we can detect it in the constraint solver val = new Constraint(undefined, {literal: true}); arr._unsubstantiated_items[key] = val; } return val.get(); }; // For internal use; set a particular item in the array var _put = function (arr, key, val) { cjs.wait(); // Don't run any nullification listeners until this function is done running var $previous_value = arr._value[key]; // If there's an unsubstantiated item; use that, so that dependencies still work if ($previous_value === undefined && arr._unsubstantiated_items[key]) { $previous_value = arr._value[key] = arr._unsubstantiated_items[key]; delete arr._unsubstantiated_items[key]; } if (is_constraint($previous_value)) { // If there was a previous value, just set it var prev_val = $previous_value.get(); $previous_value.set(val); } else { // Otherwise, just create a new value arr._value[key] = new Constraint(val, {literal: true}); } _update_len(arr); // Make sure the length hasn't changed cjs.signal(); // OK, run nullification listeners now if necessary return val; }; // Remove every element of the array var _clear = function (arr, silent) { var $val; cjs.wait(); // Keep on popping and don't stop! while (arr._value.length > 0) { $val = arr._value.pop(); var len = arr._value.length; if (is_constraint($val)) { $val.destroy(silent); // Clear memory for every element } } _update_len(arr, silent); cjs.signal(); return this; }; var _update_len = function (arr, silent) { // The setter will automatically not update if the value is the same arr.$len.set(arr._value.length, silent ? {silent:true} : false); }; /** * Change the equality check; useful for indexOf * * @method setEqualityCheck * @param {function} equality_check - A new function to check for equality between two items in this array * @return {cjs.ArrayConstraint} `this` */ proto.setEqualityCheck = function (equality_check) { this.$equality_check.set(equality_check); return this; }; /** * The forEach() method executes a provided function once per array element. * * @method forEach * @param {function} callback - Function to execute for each element. * @param {*} thisArg - Object to use as `this` when executing `callback`. * @return {cjs.ArrayConstraint} `this` * @example * var arr = cjs(['a','b','c']); * arr.forEach(function(val, i) { * console.log(val); * if(i === 1) { * return cjs.ArrayConstraint.BREAK; * } * }); // 'a' ... 'b' */ proto.forEach = function (callback, thisArg) { var i, len = this.length(); thisArg = thisArg || root; // Set thisArg to window if not specified for (i = 0; i < len; i += 1) { if (callback.call(thisArg, _get(this, i), i) === my.BREAK) { // "break" equivalent return this; } } return this; }; /** * The map() method creates a new array (not array constraint) with the results of calling a provided * function on every element in this array. * * @method map * @param {function} callback - Function that produces an element of the new Array from an element of the current one. * @param {*} thisArg - Object to use as `this` when executing `callback`. * @return {array} - The result of calling `callback` on every element * @example * var arr = cjs([1,2,3]); * arr.map(function(x) { return x+1;}) // [2,3,4] */ proto.map = function (callback, thisArg) { var rv = []; thisArg = thisArg || root; this.forEach(function(val, i) { rv[i] = callback.call(thisArg, val, i); }); return rv; }; /** * Replaces the whole array * * @method setValue * @param {array} arr - The new value * @return {cjs.ArrayConstraint} - `this` * @example * var arr = cjs([1,2,3]); * arr.toArray(); //[1,2,3] * arr.setValue(['a','b','c']); * arr.toArray(); //['a','b','c'] */ proto.setValue = function (arr) { cjs.wait(); // Don't run nullified functions quite yet _clear(this); this.push.apply(this, arr); cjs.signal(); // OK, now run them return this; }; /** * Convert my value to a standard JavaScript array * * @method item * @return {array} - A standard JavaScript array * @see toArray * @example * var arr = cjs([1,2,3]); * arr.item(); //[1,2,3] */ /** * Get item `key` * * @method item^2 * @param {number} key - The array index * @return {*} - The value at index `key` * @example * var arr = cjs(['a','b']); * arr.item(0); //['a'] */ /** * Set item i * * @method item^3 * @param {number} key - The array index * @param {*} value - The new value * @return {*} - `value` * @example * var arr = cjs(['a','b']); * arr.item(0,'x'); * arr.toArray(); // ['x','b'] */ proto.item = function (key, val) { if(arguments.length === 0) { // Just return an array if called with no arguments return this.toArray(); } else if (arguments.length === 1) { // Get if called with one argument return _get(this, key); } else if (arguments.length > 1) { // Put if called with more than one argument return _put(this, key, val); } }; /** * Clear this array and try to clean up any memory. * * @method destroy * @param {boolean} [silent=false] - If set to `true`, avoids invalidating any dependent constraints. */ proto.destroy = function (silent) { _clear(this, silent); this.$len.destroy(silent); }; /** * Get the length of the array. * * @method length * @return {number} - The length of the array * @example * var arr = cjs(['a','b']); * arr.length(); // 2 */ proto.length = function () { return this.$len.get(); // Remember that length is a constraint }; /** * The push() method mutates an array by appending the given elements and returning the new length of the array. * * @method push * @param {*} ...elements - The set of elements to append to the end of the array * @return {number} - The new length of the array * * @see pop * @see shift * @see unshift * @see splice * @example * var arr = cjs(['a','b']); * arr.push('c','d'); // 4 * arr.toArray(); // ['a','b','c','d'] */ proto.push = function () { var i, len = arguments.length, value_len = this._value.length; //Make operation atomic cjs.wait(); // Add every item that was passed in for (i = 0; i < len; i++) { _put(this, value_len+i, arguments[i]); } cjs.signal(); return this.length(); // return the new length }; /** * The pop() method removes the last element from an array and returns that element. * * @method pop * @return {*} - The value that was popped off or `undefined` * * @see push * @see shift * @see unshift * @see splice * @example * var arr = cjs(['a','b']); * arr.pop(); // 'b' * arr.toArray(); // ['a'] */ proto.pop = function () { var rv, $value = this._value.pop(); // $value should be a constraint cjs.wait(); if (is_constraint($value)) { // if it's a constraint return the value. // otherwise, return undefined rv = $value.get(); $value.destroy(); } // And set the proper length _update_len(this); // Ok, ready to go again cjs.signal(); return rv; }; /** * Converts this array to a JavaScript array * * @method toArray * @return {array} - This object as a JavaScript array * @example * var arr = cjs(['a','b']); * arr.toArray(); // ['a', 'b'] */ proto.toArray = function () { return this.map(identity); // just get every element }; /** * Returns the *first* item where calling filter is truthy * * @method indexWhere * @param {function} filter - The function to call on every item * @param {*} thisArg - Object to use as `this` when executing `callback`. * @return {number} - The first index where calling `filter` is truthy or `-1` * @example * var arr = cjs(['a','b','b']); * arr.indexWhere(function(val, i) { * return val ==='b'; * }); // 1 */ proto.indexWhere = function (filter, thisArg) { var i, len = this.length(), $val; thisArg = thisArg || this; for (i = 0; i < len; i += 1) { $val = this._value[i]; if (filter.call(thisArg, $val.get(), i)) { return i; } } return -1; // -1 if not found }; /** * Returns the *last* item where calling filter is truthy * * @method lastIndexWhere * @param {function} filter - The function to call on every item * @param {*} thisArg - Object to use as `this` when executing `callback`. * @return {number} - The last index where calling `filter` is truthy or `-1` * * @example * var arr = cjs(['a','b','a']); * arr.lastIndexWhere(function(val, i) { * return val ==='a'; * }); // 2 */ proto.lastIndexWhere = function (filter, thisArg) { var i, len = this.length(), $val; thisArg = thisArg || this; for (i = len - 1; i >= 0; i -= 1) { $val = this._value[i]; if (filter.call(thisArg, $val.get(), i)) { return i; } } return -1; // -1 if not found }; /** * Returns the *first* index of `item` * * @method indexOf * @param {*} item - The item we are searching for * @param {function} [equality_check] - How to check whether two objects are equal, defaults to the option that was passed in) * @return {number} - The item's index or `-1` * * @example * var arr = cjs(['a','b','a']); * arr.indexOf('a'); // 0 */ proto.indexOf = function (item, equality_check) { equality_check = equality_check || this.$equality_check.get(); var filter = function (x) { return equality_check(x, item); }; return this.indexWhere(filter); }; /** * Returns the *last* index of `item` * * @method lastIndexOf * @param {*} item - The item we are searching for * @param {function} [equality_check] - How to check whether two objects are equal, defaults to the option that was passed in) * @return {number} - The item's index or `-1` * @example * var arr = cjs(['a','b','a']); * arr.indexOf('a'); // 2 */ proto.lastIndexOf = function (item, equality_check) { equality_check = equality_check || this.$equality_check.get(); var filter = function (x) { return equality_check(x, item); }; return this.lastIndexWhere(filter); }; /** * Return `true` if `filter` against any item in my array is truthy * * @method some * @param {function} filter - The function to check against * @param {*} thisArg - Object to use as `this` when executing `filter`. * @return {boolean} - `true` if some item matches `filter`. `false` otherwise * @see every * @example * var arr = cjs([1,3,5]); * arr.some(function(x) { return x%2===0; }); // false */ proto.some = function(filter, thisArg) { return this.indexWhere(filter, thisArg) >= 0; }; /** * Return `true` if `filter` against every item in my array is truthy * * @method every * @param {function} filter - The function to check against * @param {*} thisArg - Object to use as `this` when executing `filter`. * @return {boolean} - `true` if some item matches `filter`. `false` otherwise * @see some * @example * var arr = cjs([2,4,6]); * arr.some(function(x) { return x%2===0; }); // true */ proto.every = function(filter, thisArg) { var rv = true; this.forEach(function() { if(!filter.apply(thisArg, arguments)) { // break on the first non-obeying element rv = false; return my.BREAK; } }); return rv; }; /** * The splice() method changes the content of an array, adding new elements while removing old elements. * * @method splice * @param {number} index - Index at which to start changing the array. If greater than the length of the array, * no elements will be removed. * @param {number} howMany - An integer indicating the number of old array elements to remove. * If howMany is 0, no elements are removed. In this case, you should specify at least one new element. * If howMany is greater than the number of elements left in the array starting at index, * then all of the elements through the end of the array will be deleted. * @param {*} ...elements - The elements to add to the array. If you don't specify any elements, * splice simply removes elements from the array. * @return {array.*} - An array containing the removed elements. If only one element is removed, * an array of one element is returned. If no elements are removed, an empty array is returned. * * @see push * @see pop * @see shift * @see unshift * @example * var arr = cjs(['a','b','c']); * arr.splice(0,2,'x','y'); //['a','b'] * arr.toArray(); // ['x','y','c'] */ proto.splice = function (index, howmany) { var i; if (!isNumber(howmany)) { howmany = 0; } if (!isPositiveInteger(index) || !isPositiveInteger(howmany)) { throw new Error("index and howmany must be positive integers"); } var to_insert = slice.call(arguments, 2), to_insert_len = to_insert.length; // Don't run any listeners until we're done cjs.wait(); // It's useful to keep track of if the resulting shift size is negative because // that will influence which direction we loop in var resulting_shift_size = to_insert_len - howmany; // removed will hold the items that were removed var removed = map(this._value.slice(index, index + howmany), function(x) { return x ? x.get() : undefined; }); // If we have to remove items if (resulting_shift_size < 0) { var value_len = this._value.length, insertion_max = index + to_insert_len, movement_max = value_len + resulting_shift_size; // If it's in the insertion range, use the user-specified insert for (i = index; i= index; i -= 1) { if (i < index + to_insert_len) { // If it's in the insertion range... _put(this, i, to_insert[i - index]); } else { // If not... _put(this, i, _get(this, i - resulting_shift_size)); } } } if(resulting_shift_size !== 0) { // Don't bother if no resulting shift _update_len(this); } cjs.signal(); // And finally run any listeners return removed; }; /** * The shift() method removes the first element from an array and returns that element. * This method changes the length of the array. * * @method shift * @return {*} - The element that was removed * * @see unshift * @see push * @see pop * @see splice * @example * var arr = cjs(['a','b','c']); * arr.shift(); // 'a' * arr.toArray(); //['b','c'] */ proto.shift = function () { var rv_arr = this.splice(0, 1); return rv_arr[0]; }; /** * The unshift() method adds one or more elements to the beginning of an array and returns the new length * of the array. * * @method unshift * @param {*} ...elements - The elements to be added * @return {number} - The new array length * * @see shift * @see push * @see pop * @see splice * @example * var arr = cjs(['a','b','c']); * arr.unshift('x','y'); // 5 * arr.toArray(); //['x','y','a','b','c'] */ proto.unshift = function () { this.splice.apply(this, ([0, 0]).concat(toArray(arguments))); return this.length(); }; /** * The concat() method returns a new array comprised of this array joined with other array(s) and/or value(s). * * @method concat * @param {*} ...values - Arrays and/or values to concatenate to the resulting array. * @return {array} The concatenated array * @example * var arr1 = cjs(['a','b','c']), * arr2 = cjs(['x']); * arr1.concat(arr2); // ['a','b','c','x'] */ proto.concat = function () { // Every argument could either be a JS array or array constraint var args = map(arguments, function(arg) { return is_array(arg) ? arg.toArray() : arg; }); var my_val = this.toArray(); return my_val.concat.apply(my_val, args); }; /** * The slice() method returns a portion of an array. * * @method slice * @param {number} [begin=0] - Zero-based index at which to begin extraction. * @param {number} [end=this.length] - Zero-based index at which to end extraction. slice extracts up to but not including end. * @return {array} A JavaScript array * @example * var arr = cjs(['a','b','c']); * arr.slice(1); // ['b','c'] */ proto.slice = function () { // Just call the normal slice with the same arguments var sliced_arr = this._value.slice.apply(this._value, arguments); return map(sliced_arr, function(x) { return x ? x.get() : undefined; }); }; /** * Return a constraint whose value is bound to my value for key * * @method itemConstraint * @param {number|Constraint} key - The array index * @return {Constraint} - A constraint whose value is `this[key]` * @example * var arr = cjs(['a','b','c']); * var first_item = arr.itemConstraint(0); * first_item.get(); // 'a' * arr.item(0,'x'); * first_item.get(); // 'x' */ proto.itemConstraint = function(key) { return new Constraint(function() { // Call cjs.get on the key so the key can also be a constraint return this.item(cjs.get(key)); }, { context: this }); }; /** * The filter() method creates a new array with all elements that pass the test implemented by the provided function. * * @method filter * @param {function} callback - Function to test each element of the array. * @param {*} [thisObject] - Object to use as this when executing callback. * @return {array} A filtered JavaScript array */ /** * The join() method joins all elements of an array into a string. * * @method join * @param {string} [separator=','] - Specifies a string to separate each element of the array. * The separator is converted to a string if necessary. If omitted, the array elements are separated with a comma. * @return {string} The joined string */ /** * The sort() method sorts the elements of an array in place and returns the array. * The default sort order is lexicographic (not numeric). * * @method sort * @param {function} [compreFunction] - Specifies a function that defines the sort order. If omitted, * the array is sorted lexicographically (in dictionary order) according to the string conversion of each element. * @return {array} A sofrted JavaScript array */ /** * The reverse() method reverses an array in place. The first array element becomes the last and the last becomes the first. * * @method reverse * @return {array} A JavaScript array whose value is the reverse of mine */ /** * The toString() method returns a string representing the specified array and its elements. * * @method toString * @return {string} A string representation of this array. */ each(["filter", "join", "sort", "reverse", "toString"], function (fn_name) { // All of these functions will just convert to an array and return that proto[fn_name] = function () { var my_val = this.toArray(); return my_val[fn_name].apply(my_val, arguments); }; }); }(ArrayConstraint)); /** @lends */ /** * Determine whether an object is an array constraint * @method cjs.isArrayConstraint * @param {*} obj - An object to check * @return {boolean} - `true` if `obj` is a `cjs.ArrayConstraint`, `false` otherwise */ is_array = function(obj) { return obj instanceof ArrayConstraint; }; extend(cjs, { /** * Create an array constraint * @method cjs.array * @constructs cjs.ArrayConstraint * @param {Object} [options] - A set of options to control how the array constraint is evaluated * @return {cjs.ArrayConstraint} - A new array constraint object * @see cjs.ArrayConstraint * @example * var arr = cjs.array({ * value: [1,2,3] * }); */ array: function (options) { return new ArrayConstraint(options); }, /** @expose cjs.ArrayConstraint */ ArrayConstraint: ArrayConstraint, /** @expose cjs.isArrayConstraint */ isArrayConstraint: is_array }); // Map Constraints // --------------- // Maps use hashing to improve performance. By default, the hash is a simple toString // function var defaulthash = function (key) { return key+""; }; // A string can also be specified as the hash, so that the hash is the result of calling // that property of the object var get_str_hash_fn = function (prop_name) { return function (key) { return key[prop_name](); }; }; /** * ***Note:*** the preferred way to create a map constraint is with `cjs.map` * This class is meant to emulate JavaScript objects ({}) but with constraints * * Options: * * - `hash`: a key hash to use to improve performance when searching for a key (default: `x.toString()`) * - `valuehash`: a value hash to use improve performance when searching for a value (default: `false`) * - `equals`: How to check for equality when searching for a key (default: `===`) * - `valueequals`: How to check for equality when searching for a value (default: `===`) * - `value`: An optional starting value (default: `{}`) * - `keys`: An optional starting set of keys (default: `[]`) * - `values`: An optional starting set of values (default: `[]`) * - `literal_values`: True if values that are functions should return a function rather than that function's return value. (default: `false`) * - `create_unsubstantiated`: Create a constraint when searching for non-existent keys. (default: `true`) * * @class cjs.MapConstraint * @classdesc A class that adds constraint to objects * @param {Object} [options] - A set of options to control how the map constraint is evaluated */ MapConstraint = function (options) { options = extend({ hash: defaulthash, // Improves performance when searching by key valuehash: false, // Function if we should hash values, which improves performance when searching by value. By default, we don't hash values equals: eqeqeq, // Equality check when searching by key valueequals: eqeqeq, // Equality check when searching by value value: {}, // Optional starting value keys: [], // Rather than passing in 'value', keys and values can be equal-length arrays specifying keys... values: [], // and values literal_values: false, // true if every value should be literal create_unsubstantiated: true // Create a value when a key isn't found }, options); options.keys = clone(options.keys); options.values = clone(options.values); var set_keys = {}; //ensure no duplicate keys each(options.keys, function(key) { set_keys[key] = true; }); // Append all of the keys and values passed to the keys and values arrays each(options.value, function (v, k) { if(!set_keys[k]) { options.keys.push(k); options.values.push(v); } }, this); set_keys = false; // Convert to boolean this._default_literal_values = !!options.literal_values; this.$equality_check = new Constraint(options.equals, {literal: true}); this.$vequality_check = new Constraint(options.valueequals, {literal: true}); // Get my hash this._hash = isString(options.hash) ? get_str_hash_fn(options.hash) : options.hash; this._create_unsubstantiated = options.create_unsubstantiated; this._khash = {}; // If we're hashing values, then set this._valuehash as a function if (options.valuehash) { this._vhash = {}; if (isFunction(options.valuehash)) { this._valuehash = options.valuehash; } else if (isString(options.valuehash)) { this._valuehash = get_str_hash_fn(options.valuehash); } else { this._valuehash = defaulthash; } } else { this._vhash = false; } var is_literal = this._default_literal_values; // Keeps track of the values and maintains the proper order this._ordered_values = map(options.keys, function (k, i) { var v = options.values[i]; // Have key (k) and value (v) var info = { key: new Constraint(k, {literal: true}), value: new Constraint(v, {literal: is_literal}), index: new Constraint(i, {literal: true}) }; // Properly put the entry into the key hash var hash = this._hash(k); var hash_val = this._khash[hash]; if (hash_val) { hash_val.push(info); } else { this._khash[hash] = [info]; } // If we hash values too, properly put the entry into the value hash if (this._vhash) { var value_hash = this._valuehash(v); var vhash_val = this._vhash[value_hash]; if (vhash_val) { vhash_val.push(info); } else { this._vhash[value_hash] = [info]; } } // And finally, set return info for this._ordered_values[i] return info; }, this); // Keeps track of requested values that aren't set this._unsubstantiated_values = {}; // Array to store keys this.$keys = new Constraint(function () { var rv = []; this.forEach(function (value, key, index) { rv[index] = key; }); return rv; }, {context: this}); // Array to store values this.$values = new Constraint(function() { var rv = []; this.forEach(function (value, key, index) { rv[index] = value; }); return rv; }, {context: this}); // Full entries (includes keys and values) this.$entries = new Constraint(function() { var rv = []; this.forEach(function (value, key, index) { rv[index] = {key: key, value: value}; }); return rv; }, {context: this}); // Number of keys this.$size = new Constraint(function() { return this._ordered_values.length; }, {context: this}); }; (function (my) { /** * Any iterator in forEach can return this object to break out of its loop. * @property {object} cjs.MapConstraint.BREAK */ my.BREAK = ArrayConstraint.BREAK; var proto = my.prototype; /** @lends cjs.MapConstraint.prototype */ // Utility function to return information about a key var _find_key = function (key, fetch_unsubstantiated, create_unsubstantiated, literal) { // Get the hash var hash = this._hash(key), rv = { h: hash, // the actual hash value hv: false, // the hash array at the hash value i: -1, // the index of the key in the hash array ui: -1, // the index in the unsubstantiated array uhv: false // the unsubstantiated hash array }, eq = this.$equality_check.get(), index_where_fn = function (a, b) { return eq(a.key.get(), key); }, hash_values = this._khash[hash]; if (hash_values) { // We found a potential hash array var key_index = indexWhere(hash_values, index_where_fn); rv.hv = hash_values; if(key_index >= 0) { // Wohoo! we also found the key in there rv.i = key_index; return rv; } } // Haven't returned yet, so we didn't find the entry. Look for an unsubstantiated // value instead. if (fetch_unsubstantiated !== false) { //Not found var unsubstantiated_values = this._unsubstantiated_values[hash], unsubstantiated_index = -1; if (unsubstantiated_values) { rv.uhv = unsubstantiated_values; unsubstantiated_index = indexWhere(unsubstantiated_values, index_where_fn); if(unsubstantiated_index >= 0) { rv.ui = unsubstantiated_index; return rv; } } // We haven't returned yet, so we didn't find an unsubstantiated value either // Check to see if we should create one. if(create_unsubstantiated === true) { var is_literal = this._default_literal_values, unsubstantiated_info = { key: new Constraint(key, {literal: true}), value: new Constraint(undefined, {literal: literal === undefined ? this._default_literal_values : !!literal}), // will be undefined index: new Constraint(-1, {literal: true}) // with a negative index }; if(unsubstantiated_values) { // The hash was found but not the particular value // Add it onto the end unsubstantiated_index = unsubstantiated_values.length; unsubstantiated_values[unsubstantiated_index] = unsubstantiated_info; } else { // The hash wasn't found; create a new array unsubstantiated_index = 0; this._unsubstantiated_values[hash] = unsubstantiated_values = [unsubstantiated_info]; } } rv.uhv = unsubstantiated_values || false; // Want to return false if not found rv.ui = unsubstantiated_index; } return rv; }; // Responsible for setting a key properly var _do_set_item_ki = function (ki, key, value, index, literal) { // ki is the key information from _find_key var i, value_hash, vhash_val, info, key_index = ki.i, // where the key is in the hash array hash_values = ki.hv, // the hash array hash = ki.h; // the hash value if (key_index >= 0) { // The key was already in this map // get the information info = hash_values[key_index]; if (this._vhash) { // If we're hashing values, the new value has to get re-hashed var old_value = info.value.get(), old_value_hash = this._valuehash(old_value), old_vhash_val = this._vhash[old_value_hash]; value_hash = this._valuehash(value); if (old_vhash_val) { // This should probably always be true, unless something went wrong... var len = old_vhash_val.length; for (i = 0; i < len; i += 1) { if (old_vhash_val[i] === info) { // wohoo, found it old_vhash_val.splice(i, 1); if (old_vhash_val.length === 0) { delete this._vhash[old_value_hash]; // don't keep the old hash array } break; } } } // Put the new value has in vhash_val = this._vhash[value_hash]; // hash array if (vhash_val) { vhash_val.push(info); // add onto the hash array } else { this._vhash[value_hash] = [info]; // create a new hash array } } info.value.set(value); // set the value constraint to the new value if (isPositiveInteger(index)) { // But they also specified an index... var old_index = info.index.get(); if(old_index !== index) { // great...now we have to move it too // take out the old value this._ordered_values.splice(old_index, 1); // and re-add it this._ordered_values.splice(index, 0, info); // Properly iterate regardless of whether moving higher or lower var low = Math.min(old_index, index); var high = Math.max(old_index, index); // update the indicies of every thing between that might have been affected for (i = low; i <= high; i += 1) { _set_index(this._ordered_values[i], i); } this.$keys.invalidate(); // Keys are now invalid } } } else { // They didn't specify an index or at least they specified it wrong... if (!isPositiveInteger(index)) { index = this._ordered_values.length; // just set it to the } // Check to see if there was an unsubstantiated item var unsubstantiated_index = ki.ui; if (unsubstantiated_index >= 0) { // Found it! Now let's remove it from the list of unsubstantiated items var unsubstantiated_hash_values = ki.uhv, unsubstantiated_info = unsubstantiated_hash_values[unsubstantiated_index]; unsubstantiated_hash_values.splice(unsubstantiated_index, 1); if (unsubstantiated_hash_values.length === 0) { delete this._unsubstantiated_values[hash]; } info = unsubstantiated_info; // re-use the same object to keep dependencies } else { // Nothing in unsubstantiated; just create it from scratch info = { key: new Constraint(key, {literal: true}), value: new Constraint(value, {literal: literal === undefined ? this._default_literal_values : !!literal}), index: new Constraint(index, {literal: true}) }; } if(hash_values) { // There was already a hash array hash_values.push(info); } else { // Have to create the hash array hash_values = this._khash[hash] = [info]; } //If we're hashing values... if (this._vhash) { value_hash = this._valuehash(value); vhash_val = this._vhash[value_hash]; // Add the item to the value hash if (vhash_val) { vhash_val.push(info); } else { this._vhash[value_hash] = [info]; } } // insert into values this._ordered_values.splice(index, 0, info); if (unsubstantiated_index >= 0) { info.value.set(value); // but update its value and index info.index.set(index); } // Push the index of every item that I spliced before up for (i = index + 1; i < this._ordered_values.length; i += 1) { _set_index(this._ordered_values[i], i); } // Now, size and keys are invalid this.$size.invalidate(); this.$keys.invalidate(); } this.$values.invalidate(); this.$entries.invalidate(); }; // Cange an info's specified index var _set_index = function (info, to_index) { info.index.set(to_index); }; // Deallocate memory from constraints var _destroy_info = function (infos, silent) { each(infos, function (info) { info.key.destroy(silent); info.value.destroy(silent); info.index.destroy(silent); }); }; // removes the selected item and destroys its value to deallocate it var _remove_index = function (index, silent) { var info = this._ordered_values[index]; _destroy_info(this._ordered_values.splice(index, 1), silent); if(silent !== true) { this.$size.invalidate(); } }; /** * Get the keys on this object. * * @method keys * @return {array.*} - The set of keys * @see values * @see entries * @example * var map = cjs({x: 1, y: 2}); * map.keys(); // ['x','y'] */ proto.keys = function () { return this.$keys.get(); }; /** * Get the values on this object. * * @method values * @return {array.*} - The set of values * @see keys * @see entries * @example * var map = cjs({x: 1, y: 2}); * map.values(); // [1,2] */ proto.values = function () { return this.$values.get(); }; /** * Get every key and value of this object as an array. * * @method entries * @return {array.object} - A set of objects with properties `key` and `value` * @see keys * @see values * @example * var map = cjs({x: 1, y: 2}); * map.entries(); // [{key:'x',value:1}, * // {key:'y',value:2}] */ proto.entries = function () { return this.$entries.get(); }; /** * Get the number of entries in this object. * * @method size * @return {number} - The number of entries * @see isEmpty * @example * var map = cjs({x: 1, y: 2}); * map.size(); // 2 */ proto.size = function () { return this.$size.get(); }; /** * Check if this object has any entries * * @method isEmpty * @return {boolean} - `true` if there are no entries, `false` otherwise * @see size * @example * var map = cjs({x: 1, y: 2}); * map.isEmpty(); // false */ proto.isEmpty = function () { return this.size() === 0; }; /** * Set the entry for `key` to `value` (`this[key]=value`) * * @method put * @param {*} key - The entry's key * @param {*} value - The entry's value * @param {number} [index=this.size] - The entry's index * @param {boolean} [literal] - Whether to treat the value as literal * @return {cjs.MapConstraint} - `this` * @see get * @see getOrPut * @see item * @see remove * @see clear * * @example * var map = cjs({x: 1, y: 2}); * map.put("z", 3, 1); * map.keys(); // ['x','z','y'] */ proto.put = function (key, value, index, literal) { cjs.wait(); // Find out if there's a key or unsubstantiated info but don't create it var ki = _find_key.call(this, key, true, false, literal); // And do the work of putting _do_set_item_ki.call(this, ki, key, value, index, literal); cjs.signal(); return this; }; /** * Remove a key's entry (like `delete this[key]`) * * @method remove * @param {*} key - The entry's key * @return {cjs.MapConstraint} - `this` * * @see put * @see clear * * @example * var map = cjs({x: 1, y: 2}); * map.remove("x"); * map.keys(); // ['y'] */ proto.remove = function (key, silent) { // Find out if there's an actual key set var ki = _find_key.call(this, key, false, false), key_index = ki.i, hash_values = ki.hv, i, info, ordered_index, value_hash, vhash_val; // If the item was found if (key_index >= 0) { cjs.wait(); info = hash_values[key_index]; // The info about the value ordered_index = info.index.get(); // The map's index (not the index in the hash array) hash_values.splice(key_index, 1); // Remove info from the hash array if (hash_values.length === 0) { // If there isn't anything in the hash array, delete this._khash[ki.h]; // remove it } // If the value is also hashed.. if (this._vhash) { // Find the value hash information value_hash = this._valuehash(info.value.get()); // the lookup key for the value hash vhash_val = this._vhash[value_hash]; // the value hash array if (vhash_val) { // Found the value hash var len = vhash_val.length; for (i = 0; i < len; i += 1) { if (vhash_val[i] === info) { // found the actual item vhash_val.splice(i, 1); // remove it from the array if (vhash_val.length === 0) { delete this._vhash[value_hash]; // and if it's empty, remove the whole value hash array } break; // Wohoo! } } } } _remove_index.call(this, ordered_index, silent); // remove ordered_index (splices the ordered array) for (i = ordered_index; i < this._ordered_values.length; i += 1) { _set_index(this._ordered_values[i], i); // and update the index for every item } // And now all of these constraint variables are invalid. if(!silent) { this.$size.invalidate(); this.$keys.invalidate(); this.$values.invalidate(); this.$entries.invalidate(); } // OK, now you can run any nullified listeners cjs.signal(); } return this; }; /** * Get the item at key (like this[key]) * * @method get * @param {*} key - The entry's key * @return {*|undefined} - the value at that entry or `undefined` * * @see item * @see put * @see getOrPut * * @example * var map = cjs({x: 1, y: 2}); * map.get("x"); // 1 */ proto.get = function (key) { // Try to find the key and search in any unsubstantiated values var ki = _find_key.call(this, key, true, this._create_unsubstantiated), key_index = ki.i, hash_values = ki.hv; if (key_index >= 0) { // Found it; get the item in the hash's value var info = hash_values[key_index]; return info.value.get(); } else if(this._create_unsubstantiated) { var unsubstantiated_info = ki.uhv[ki.ui]; // use the unsubstantiated getter to create a dependency return unsubstantiated_info.value.get(); } else { // not found and can't create unsubstantiated item return undefined; } }; /** * Convert my value to a standard JavaScript object. The keys are converted using `toString` * * @method item * @return {object} - A standard JavaScript object * @see toObject * @example * var map = cjs({x: 1, y: 2}); * map.item(); // {x:1,y:2} */ /** * Get item `key` * * @method item^2 * @param {number} key - The object key * @return {*} - The value at index `key` * * @see get * @see put * @see getOrPut * * @example * var map = cjs({x: 1, y: 2}); * map.item('x'); // 1 */ /** * Set item i * * @method item^3 * @param {number} key - The object key * @param {*} value - The new value * @return {cjs.MapConstraint} - `this` * * @see get * @see put * @see getOrPut * * @example * var map = cjs({x: 1, y: 2}); * map.item('z', 3); * map.keys(); //['x','y','z'] */ proto.item = function (arg0, arg1, arg2) { if(arguments.length === 0) { // no arguments? return an object return this.toObject(); } else if (arguments.length === 1) { // One, try to get the keys values return this.get(arg0); } else { // more than two, try to set return this.put(arg0, arg1, arg2); } }; /** * Return a constraint whose value is bound to my value for key * * @method itemConstraint * @param {*|Constraint} key - The array index * @return {Constraint} - A constraint whose value is `this[key]` * * @example * var map = cjs({x: 1, y: 2}); * var x_val = map.itemConstraint('x'); * x_val.get(); // 1 * map.item('x', 3); * x_val.get(); // 3 */ proto.itemConstraint = function(key) { return new Constraint(function() { // Call cjs.get on the key so the key can also be a constraint return this.get(cjs.get(key)); }, { context: this }); }; /** * Clear every entry of this object. * * @method clear * @return {cjs.MapConstraint} - `this` * @see remove * @see isEmpty * @example * var map = cjs({x: 1, y: 2}); * map.isEmpty(); // false * map.clear(); * map.isEmpty(); // true */ proto.clear = function (silent) { if (this.size() > 0) { // If I actually have something cjs.wait(); // Keep removing items while (this._ordered_values.length > 0) { _remove_index.call(this, 0, silent); } // And get rid of every key hash each(this._khash, function (arr, hash) { delete this._khash[hash]; }, this); // and value hash if applicable if (this._vhash) { each(this._vhash, function (arr, hash) { delete this._vhash[hash]; }, this); } // and everything should be invalid if(!silent) { this.$keys.invalidate(); this.$values.invalidate(); this.$entries.invalidate(); this.$size.invalidate(); } cjs.signal(); // ready to run nullification listeners } return this; }; /** * The forEach() method executes a provided function once per entry. * If cjs.MapConstraint.BREAK is returned for any element, we stop looping * * @method forEach * @param {function} callback - Function to execute for each entry. * @param {*} thisArg - Object to use as `this` when executing `callback`. * @return {cjs.MapConstraint} - `this` * @example * var map = cjs({x:1,y:2,z:3}); * map.forEach(function(val, key) { * console.log(key+':'+val); * if(key === 'y') { * return cjs.MapConstraint.BREAK; * } * }); // x:1 ... y:2 */ proto.forEach = function (func, thisArg) { var i, info, len = this.size(), ov_clone = this._ordered_values.slice(); thisArg = thisArg || this; for (i = 0; i < len; i += 1) { info = ov_clone[i]; if (info && func.call(thisArg, info.value.get(), info.key.get(), info.index.get()) === my.BREAK) { // break if desired break; } } return this; }; /** * Change the default equality check when getting a key * * @method setEqualityCheck * @param {function} equality_check - The new key equality check * @return {cjs.ArrayConstraint} - `this` */ proto.setEqualityCheck = function (equality_check) { this.$equality_check.set(equality_check); return this; }; /** * Change the default value equality check when getting a value * * @method setValueEqualityCheck * @param {function} vequality_check - The new value equality check * @return {cjs.ArrayConstraint} - `this` */ proto.setValueEqualityCheck = function (vequality_check) { this.$vequality_check.set(vequality_check); return this; }; /** * Change the hash function when getting a key * * @method setHash * @param {function|string} hash - The new hashing function (or a string representing a property name for every key to use as the hash) * @return {cjs.ArrayConstraint} - `this` */ proto.setHash = function (hash) { cjs.wait(); // First, empty out the old key hash and unsubstantiated values this._hash = isString(hash) ? get_str_hash_fn(hash) : hash; this._khash = {}; // Then, for every one of my values, re-hash each(this._ordered_values, function (info) { var key = info.key.get(); var hash = this._hash(key); var hash_val = this._khash[hash]; if (hash_val) { hash_val.push(info); } else { this._khash[hash] = [info]; } }, this); // And re-hash for every unsubstantiated value var new_unsubstantiated_values = {}; each(this._unsubstantiated_values, function(unsubstantiated_value_arr) { each(unsubstantiated_value_arr, function(info) { var key = info.key.get(); var hash = this._hash(key); var hash_val = this.new_unsubstatiated_values[hash]; if(hash_val) { hash_val.push(info); } else { new_unsubstantiated_values[hash] = [info]; } }, this); }, this); this._unsubstantiated_values = new_unsubstantiated_values; cjs.signal(); return this; }; /** * Change the hash function when getting a value * * @method setValueHash * @param {function|string} hash - The new hashing function (or a string representing a property name for every key to use as the hash) * @return {cjs.ArrayConstraint} - `this` */ proto.setValueHash = function (vhash) { this._valuehash = isString(vhash) ? get_str_hash_fn(vhash) : vhash; // Empty out the old value hash this._vhash = {}; if (this._valuehash) { // And reset the value hash for every element each(this._ordered_values, function (info) { var value = info.value.get(); var hash = this._valuehash(value); var hash_val = this._vhash[hash]; if (hash_val) { hash_val.push(info); } else { this._vhash[hash] = [info]; } }, this); } return this; }; /** * Get the index of the entry with key = `key` * * @method indexOf * @param {*} key - The key to search for. * @return {number} - The index of the entry with key=`key` or `-1` * * @example * var map = cjs({x: 1, y: 2}); * map.indexOf('z'); // -1 */ proto.indexOf = function (key) { // get hash information var ki = _find_key.call(this, key, true, this._create_unsubstantiated), key_index = ki.i, hash_values = ki.hv; if (key_index >= 0) { // Found! return the proper item's index var info = hash_values[key_index]; return info.index.get(); } else if(ki.ui >= 0) { // Not found but creating unsubstantiated items var unsubstantiated_info = ki.uhv[ki.ui]; return unsubstantiated_info.index.get(); // create a dependency } else { // Not found and not creating unsubstantiated items return -1; } }; /** * Search for a key or create it if it wasn't found * * @method getOrPut * @param {*} key - The key to search for. * @param {function} create_fn - A function to create the value if `key` is not found * @param {*} [create_fn_context] - The context in which to call `create_fn` * @param {number} [index=this.size] - Where to place a value that is created * @param {boolean} [literal=false] - Whether to create the value as a literal constraint * (the value of a function is the function) * @return {number} - The index of the entry with key=`key` or `-1` * * @see get * @see put * @see item * @example * var map = xjs({x: 1, y: 2}); * map.getOrPut('x', function() { * console.log("evaluating"); * return 3; * }); // output: 'evaluating' * // 3 * map.getOrPut('x', function() { * console.log("evaluating"); * return 3; * }); // (no output) * // 3 */ proto.getOrPut = function (key, create_fn, create_fn_context, index, literal) { var ki = _find_key.call(this, key, true, false, literal), key_index = ki.i, // index within hash array hash_values = ki.hv, // hash array hash = ki.h, // hash value context, value, info; if (key_index >= 0) { // found actual item! info = hash_values[key_index]; return info.value.get(); } else { // need to create it cjs.wait(); context = create_fn_context || this; value = create_fn.call(context, key); // will set the value to this _do_set_item_ki.call(this, ki, key, value, index, literal); // do the work of putting cjs.signal(); return value; } }; /** * Check if there is any entry with key = `key` * * @method has * @param {*} key - The key to search for. * @return {boolean} - `true` if there is an entry with key=`key`, `false` otherwise. * * @see get * @see item * @example * var map = cjs({x: 1, y: 2}); * map.has('x'); // true */ proto.has = function (key) { var ki = _find_key.call(this, key, true, this._create_unsubstantiated); var key_index = ki.i; if (key_index >= 0) { // Found successfully return true; } else if(this._create_unsubstantiated) { // Didn't find but there is an unsubstantiated item var unsubstantiated_info = ki.uhv[ki.ui]; unsubstantiated_info.index.get(); // Add a dependency return false; } else { // No dependency to be added; just say we didn't find it return false; } }; /** * Move the entry at `old_index` to index `new_index` * * @method moveIndex * @param {number} old_index - The index to move from * @param {number} new_index - The index to move to * @return {cjs.ArrayConstraint} - `this` * @example * var map = cjs({x: 1, y: 2, z: 3}); * map.keys(); // ['x','y', 'z'] * map.moveIndex(1, 0) * map.keys(); // ['y','x', 'z'] */ proto.moveIndex = function (old_index, new_index) { var i; cjs.wait(); var info = this._ordered_values[old_index]; // take out the old value this._ordered_values.splice(old_index, 1); // and re-add it this._ordered_values.splice(new_index, 0, info); // Properly iterate regardless of whether moving higher or lower var low = Math.min(old_index, new_index); var high = Math.max(old_index, new_index); // update the indicies of every thing between that might have been affected for (i = low; i <= high; i += 1) { _set_index(this._ordered_values[i], i); } // Invalidate the relevant properties (size shouldn't change) this.$keys.invalidate(); this.$values.invalidate(); this.$entries.invalidate(); cjs.signal(); return this; }; /** * Move the entry with key `key` to `index * * @method move * @param {*} key - The key to search for * @param {number} to_index - The new index for the key * @return {cjs.ArrayConstraint} - `this` * @example * var map = cjs({x: 1, y: 2, z: 3}); * map.keys(); // ['x','y', 'z'] * map.move('z', 0) * map.keys(); // ['z','x', 'y'] */ proto.move = function (key, to_index) { //Move a key to a new index var ki = _find_key.call(this, key, false, false); var key_index = ki.i; if (key_index >= 0) { var info = ki.hv[key_index]; // leverage the previous move_index function this.moveIndex(info.index.get(), to_index); } return this; }; /** * Given a value, find the corresponding key * * @method keyForValue * @param {*} value - The value whose key to search for * @param {function} [eq_check] - How to check if two values are equal (default: `===` * @return {*|undefined} - The key where `this.get(key)===value` * @example * var map = cjs({x: 1, y: 2, z: 3}); * map.keyForValue(1); // 'x' */ proto.keyForValue = function (value, eq_check) { eq_check = eq_check || this.$vequality_check.get(); var i; // It's advantageous here to use a value hash if it's there if (this._vhash) { var value_hash = this._valuehash(value); var vhash_val = this._vhash[value_hash]; // Find that value hash's array if (vhash_val) { var len = vhash_val.length; for (i = 0; i < len; i += 1) { var info = vhash_val[i]; if (eq_check(info.value.get(), value)) { // found it! here's the key return info.key.get(); } } } // Didn't find it return undefined; } else { // Without a value hash, we have to iterate through every item var key; this.forEach(function (v, k) { if (eq_check(value, v)) { // found key = k; return my.BREAK; // Break out of the forEach } }); return key; } }; /** * Clear this object and try to clean up any memory. * * @method destroy * @param {boolean} [silent=false] - If set to `true`, avoids invalidating any dependent constraints. */ proto.destroy = function (silent) { cjs.wait(); this.clear(silent); this.$equality_check.destroy(silent); this.$vequality_check.destroy(silent); this.$keys.destroy(silent); this.$values.destroy(silent); this.$entries.destroy(silent); this.$size.destroy(silent); cjs.signal(); }; /** * Converts this array to a JavaScript object. * * @method toObject * @param {function} [key_map_fn] - A function to convert keys * @return {object} - This object as a JavaScript object * @example * var map = cjs({x: 1, y: 2, z: 3}); * map.toObject(); // {x:1,y:2,z:3} */ proto.toObject = function (key_map_fn) { var rv = {}; key_map_fn = key_map_fn || identity; // just use the key if not supplied this.forEach(function (v, k) { rv[key_map_fn(k)] = v; }); return rv; }; }(MapConstraint)); /** @lends */ /** * Determine whether an object is a map constraint * @method cjs.isMapConstraint * @param {*} obj - An object to check * @return {boolean} - `true` if `obj` is a `cjs.MapConstraint`, `false` otherwise */ is_map = function(obj) { return obj instanceof MapConstraint; }; extend(cjs, { /** * Create a map constraint * @method cjs.map * @constructs cjs.MapConstraint * @param {Object} [options] - A set of options to control how the map constraint is evaluated * @return {cjs.MapConstraint} - A new map constraint object * @see cjs.MapConstraint * @example Creating a map constraint * * var map_obj = cjs.map({ * value: { foo: 1 } * }); * cobj.get('foo'); // 1 * cobj.put('bar', 2); * cobj.get('bar') // 2 */ map: function (arg0, arg1) { return new MapConstraint(arg0, arg1); }, /** @expose cjs.MapConstraint */ MapConstraint: MapConstraint, /** @expose cjs.isMapConstraint */ isMapConstraint: is_map }); // Liven // ----- // Will automatically call the provided function when it becomes invalid extend(cjs, { /** * Memoize a function to avoid unnecessary re-evaluation. Its options are: * * - `context`: The context in which `func` should be evaluated * - `run_on_create`: Whether to run `func` immediately after creating the live function. (default: `true`) * - `pause_while_running`: Whether to explicitly prevent this live function from being called recursively (default: `false`) * - `on_destroy`: A function to call when `destroy` is called (default: `false`) * * The return value of this method also has two functions: * - `pause`: Pause evaluation of the live function * - `resume`: Resume evaluation of the live function * - `run`: Run `func` if it's invalid * * @method cjs.liven * @param {function} func - The function to make live * @param {object} [options] - A set of options to control how liven works * @return {object} An object with properties `destroy`, `pause`, `resume`, and `run` * * @example * var x_val = cjs(0); * var api_update = cjs.liven(function() { * console.log('updating other x'); * other_api.setX(x_val); * }); // 'updating other x' * x_val.set(2); // 'updating other x' * */ liven: function (func, options) { options = extend({ context: root, // what to equate `this` to run_on_create: true, // whether it should run immediately pause_while_running: false, // whether to allow the function to be called recursively (indirectly) priority: false, on_destroy: false // a function to call when this liven function is destroyed }, options); //Make constraint-aware values just by calling func in a constraint var node = new Constraint(func, { context: options.context, cache_value: false, auto_add_outgoing_dependencies: false, run_on_add_listener: false }); // check if running var paused = false; var do_get; var invalidate = function() { node.invalidate(); }; // Destroy the node and make sure no memory is allocated var destroy = function (silent) { if(options.on_destroy) { options.on_destroy.call(options.context, silent); } node.destroy(silent); }; // Stop changing and remove it from the event queue if necessary var pause = function () { if(paused === false) { paused = true; node.offChange(do_get); return true; // successfully paused } return false; }; // Re-add to the event queue var resume = function () { if(paused === true) { paused = false; node.onChangeWithPriority(options.priority, do_get); if(options.run_on_create !== false) { if (constraint_solver.semaphore >= 0) { node.get(false); } else { each(node._changeListeners, constraint_solver.add_in_call_stack, constraint_solver); } } return true; // successfully resumed } return false; }; // The actual getter, will call the constraint's getter do_get = function () { if (options.pause_while_running) { pause(); } node.get(); if (options.pause_while_running) { resume(); } }; // When the value changes, call do_get node.onChangeWithPriority(options.priority, do_get); var rv = { destroy: destroy, pause: pause, resume: resume, run: function(arg0) { do_get(arg0); return this; }, invalidate: invalidate, _constraint: node // for debugging purposes }; if(options.run_on_create !== false) { if (constraint_solver.semaphore >= 0) { node.get(false); } else { each(node._changeListeners, constraint_solver.add_in_call_stack, constraint_solver); } } return rv; } }); // A function to hash the arguments passed in. By default, just a concatenation of the arguments' string value var memoize_default_hash = function () { var i, len = arguments.length; var rv = ""; for (i = 0; i < len; i += 1) { rv += arguments[i]; } return rv; }, // A function to check if two sets of arguments are equal; by default just check every value memoize_default_equals = function (args1, args2) { var i, len = args1.length; if (len === args2.length) { for (i = 0; i < len; i += 1) { var arg1 = args1[i], arg2 = args2[i]; if (arg1 !== arg2) { return false; } } return true; } else { return false; } }; extend(cjs, { /** * Memoize a function to avoid unnecessary re-evaluation. Its options are: * * - `hash`: Create a unique value for each set of arguments (call with an argument array) * - `equals`: check if two sets of arguments are equal (call with two argument arrays) * - `context`: The context in which `getter_fn` should be evaluated * - `literal_values`: Whether values should be literal if they are functions * * The return value of this method also has two functions: * - `each`: Iterate through every set of arguments and value that is memoized * - `destroy`: Clear the memoized values to clean up memory * * @method cjs.memoize * @param {function} getter_fn - The function to memoize * @param {object} [options] - A set of options to control how memoization works * @return {function} The memoized function * * @example * * var arr = cjs([3,2,1,4,5,10]), * get_nth_largest = cjs.memoize(function(n) { * console.log('recomputing'); * var sorted_arr = arr memoized fn.sort(); * return sorted_arr[ny]; * }); * * get_nth_largest(0); // logfged: recomputing * get_nth_largest(0); //ulli (nothing logged because answer memoized) * arr.splice(0, 1); // N * get_nth_largest(0); // logged: recomputing */ memoize: function (getter_fn, options) { options = extend({ hash: memoize_default_hash, equals: memoize_default_equals, context: root, literal_values: true }, options); // Map from args to value options.args_map = new MapConstraint({ hash: options.hash, equals: options.equals, literal_values: options.literal_values }); // When getting a value either create a constraint or return the existing value var rv = function () { var args = slice.call(arguments), constraint = options.args_map.getOrPut(args, function() { return new Constraint(function () { return getter_fn.apply(options.context, args); }); }); return constraint.get(); }; // Clean up memory after self rv.destroy = function (silent) { options.args_map.forEach(function (constraint) { constraint.destroy(silent); }); options.args_map.destroy(silent); }; // Run through every argument and call fn on it rv.each = function (fn) { options.args_map.forEach(fn); }; rv.options = options; return rv; } }); var make_node = function(item) { // Check if the argument is a DOM node or create a new textual node with its contents if(isAnyElement(item)) { return item; } else { var node = doc.createTextNode(item); return node; } }, insert_at = function(child_node, parent_node, index) { // Utility function to insert child_node as the index-th child of parent_node var children = parent_node.childNodes; if(children.length <= index) { parent_node.appendChild(child_node); } else { var before_child = children[index]; parent_node.insertBefore(child_node, before_child); } }, remove_index = function(parent_node, index) { // Utility to remove a child DOM node by index var children = parent_node.childNodes, child_node; if(children.length > index) { child_node = children[index]; parent_node.removeChild(child_node); return child_node; } }, move_child = function(parent_node, to_index, from_index) { // Utility to move a child DOM node by indices var children = parent_node.childNodes; if(children.length > from_index) { var child_node = children[from_index]; if(parent_node) { if(from_index < to_index) { //If it's less than the index we're inserting at... to_index++; //Increase the index by 1, to make up for the fact that we're removing me at the beginning } insert_at(child_node, parent_node, to_index); } return child_node; } }, // Check if jQuery is available is_jquery_obj = function(x) { return has(root, "jQuery") ? (x instanceof root.jQuery) : false; }, nList = root.NodeList || false, // a node list is what is returned when you call getElementsByTagName, etc. isNList = nList ? function(x) { return x instanceof nList; } : function() { return false; }, // Convert an object that can be passed into a binding into an array of dom elements get_dom_array = function(obj) { if(isArray(obj)) { // already an array return obj; } else if (is_constraint(obj)) { // regular constraint return get_dom_array(obj.get()); } else if(is_array(obj)) { // array constraint return obj.toArray(); } else if(is_map(obj)) { // map constraint return obj.values(); } else if(is_jquery_obj(obj)) { // jQuery object return root.jQuery.makeArray(obj); } else if(isNList(obj)) { // node list return toArray(obj); } else { // hopefully just an element; return its value as an array return [obj]; } }; /** * A binding calls some arbitrary functions passed into options. It is responsible for keeping some aspect of a * DOM node in line with a constraint value. For example, it might keep an element's class name in sync with a * class_name constraint * * @private * @class cjs.Binding * @param {object} options * @classdesc Bind a DOM node property to a constraint value */ var Binding = function(options) { this.options = options; this.targets = options.targets; // the DOM nodes var setter = options.setter, // a function that sets the attribute value getter = options.getter, // a function that gets the attribute value init_val = options.init_val, // the value of the attribute before the binding was set curr_value, // used in live fn last_value, // used in live fn old_targets = [], // used in live fn do_update = function() { this._timeout_id = false; // Make it clear that I don't have a timeout set var new_targets = filter(get_dom_array(this.targets), isAnyElement); // update the list of targets if(has(options, "onChange")) { options.onChange.call(this, curr_value, last_value); } // For every target, update the attribute each(new_targets, function(target) { setter.call(this, target, curr_value, last_value); }, this); // track the last value so that next time we call diff last_value = curr_value; }; this._throttle_delay = false; // Optional throttling to improve performance this._timeout_id = false; // tracks the timeout that helps throttle if(isFunction(init_val)) { // If init_val is a getter, call it on the first element last_value = init_val(get_dom_array(this.targets[0])); } else { // Otherwise, just take it as is last_value = init_val; } this.$live_fn = cjs.liven(function() { curr_value = getter(); // get the value once and inside of live fn to make sure a dependency is added if(this._throttle_delay) { // We shouldn't update values right away if(!this._timeout_id) { // If there isn't any timeout set yet, then set a timeout to delay the call to do update this._timeout_id = sTO(bind(do_update, this), this._throttle_delay); } } else { // we can update the value right away if no throttle delay is set do_update.call(this); } }, { context: this }); }; (function(my) { /** @lends cjs.Binding.prototype */ var proto = my.prototype; /** * Pause binding (no updates to the attribute until resume is called) * * @method pause * @return {Binding} `this` * @see resume * @see throttle */ proto.pause = function() { this.$live_fn.pause(); return this; }; /** * Resume binding (after pause) * * @method resume * @return {Binding} `this` * @see pause * @see throttle */ proto.resume = function() { this.$live_fn.resume(); return this; }; /** * Require at least `min_delay` milliseconds between setting the attribute * * @method throttle * @param {number} min_delay - The minimum number of milliseconds between updates * @return {Binding} `this` * @see pause * @see resume */ proto.throttle = function(min_delay) { this._throttle_delay = min_delay > 0 ? min_delay : false; // Make sure it's positive if(this._timeout_id && !this._throttle_delay) { // If it was specified that there should be no delay and we are waiting for a re-eval cTO(this._timeout_id); // then prevent that re-eval this._timeout_id = false; } // regardless, run the live fn again this.$live_fn.run(); return this; }; /** * Stop updating the binding and try to clean up any memory * * @method destroy * @return {undefined} * @see pause * @see resume * @see throttle */ proto.destroy = function() { this.$live_fn.destroy(); if(this.options.onDestroy) { this.options.onDestroy(); } if(this.options.coreDestroy) { this.options.coreDestroy(); } }; }(Binding)); /** @lends */ // Creates a type of binding that accepts any number of arguments and then sets an attribute's value to depend on // every element that was passed in var create_list_binding = function(list_binding_getter, list_binding_setter, list_binding_init_value) { return function(elements) { // The first argument is a list of elements var args = slice.call(arguments, 1), // and the rest are values val = cjs(function() { // Create a constraint so that the binding knows of any changes return list_binding_getter(args); }); var binding = new Binding({ targets: elements, getter: bind(val.get, val), // use the constraint's value as the getter setter: list_binding_setter, init_val: list_binding_init_value, coreDestroy: function() { val.destroy(); // Clean up the constraint when we are done } }); return binding; }; }, create_textual_binding = function(setter) { // the text value of a node is set to the concatenation of every argument return create_list_binding(function(args) { return map(args, cjs.get).join(""); }, function(element, value) { setter(element, value); }); }, // a binding that accepts either a key and a value or an object with any number of keys and values create_obj_binding = function(obj_binding_setter) { return function(elements) { var vals, args = slice.call(arguments, 1); if(args.length === 0) { // need at least one argument return; } else if(args.length === 1) { // an object with keys and values was passed in vals = args[0]; } else if(args.length > 1) { // the first argument was the key, the second was a value vals = {}; vals[args[0]] = args[1]; } var binding = new Binding({ targets: elements, setter: function(element, value) { each(value, function(v, k) { obj_binding_setter(element, k, v); }); }, getter: function() { if(is_map(vals)) { return vals.toObject(); } else { var rv = {}; each(vals, function(v, k) { rv[k] = cjs.get(v); }); return rv; } } }); return binding; }; }; /** * Constrain a DOM node's text content * * @method cjs.bindText * @param {dom} element - The DOM element * @param {*} ...values - The desired text value * @return {Binding} - A binding object * @example If `my_elem` is a dom element * * var message = cjs('hello'); * cjs.bindText(my_elem, message); */ var text_binding = create_textual_binding(function(element, value) { // set the escaped text of a node setTextContent(element, value); }), /** * Constrain a DOM node's HTML content * * @method cjs.bindHTML * @param {dom} element - The DOM element * @param {*} ...values - The desired html content * @return {Binding} - A binding object * @example If `my_elem` is a dom element * * var message = cjs('hello'); * cjs.bindHTML(my_elem, message); */ html_binding = create_textual_binding(function(element, value) { // set the non-escaped inner HTML of a node element.innerHTML = value; }), /** * Constrain a DOM node's value * * @method cjs.bindValue * @param {dom} element - The DOM element * @param {*} ...values - The value the element should have * @return {Binding} - A binding object * @example If `my_elem` is a text input element * * var value = cjs('hello'); * cjs.bindValue(my_elem, message); */ val_binding = create_textual_binding(function(element, value) { // set the value of a node element.val = value; }), /** * Constrain a DOM node's class names * * @method cjs.bindClass * @param {dom} element - The DOM element * @param {*} ...values - The list of classes the element should have. The binding automatically flattens them. * @return {Binding} - A binding object * @example If `my_elem` is a dom element * * var classes = cjs('class1 class2'); * cjs.bindClass(my_elem, classes); */ class_binding = create_list_binding(function(args) { // set the class of a node return flatten(map(args, cjs.get), true); }, function(element, value, old_value) { // Compute difference so that old class values remain var ad = get_array_diff(old_value, value), curr_class_name = " " + element.className + " "; // add spaces so that the replace regex doesn't need extra logic // take out all of the removed classes each(ad.removed, function(removed_info) { curr_class_name = curr_class_name.replace(" " + removed_info.from_item + " ", " "); }); // and add all of the added classes curr_class_name += map(ad.added, function(x) { return x.item; }).join(" "); curr_class_name = trim(curr_class_name); // and trim to remove extra spaces element.className = curr_class_name; // finally, do the work of setting the class }, []), // say that we don't have any classes to start with /** * Constrain a DOM node's children * * @method cjs.bindChildren * @param {dom} element - The DOM element * @param {*} ...elements - The elements to use as the constraint. The binding automatically flattens them. * @return {Binding} - A binding object * @example If `my_elem`, `child1`, and `child2` are dom elements * * var nodes = cjs(child1, child2); * cjs.bindChildren(my_elem, nodes); */ children_binding = create_list_binding(function(args) { var arg_val_arr = map(args, cjs.get); return map(flatten(arg_val_arr, true), make_node); }, function(element, value, old_value) { var ad = get_array_diff(old_value, value); each(ad.removed, function(removed_info) { var child_node = remove_index(element, removed_info.from); if(this.options.onRemove) { this.options.onRemove.call(this, child_node, removed_info.from); } }, this); each(ad.added, function(added_info) { var child_node = added_info.item; insert_at(child_node, element, added_info.to); if(this.options.onAdd) { this.options.onAdd.call(this, child_node, added_info.to); } }, this); each(ad.moved, function(moved_info) { var child_node = move_child(element, moved_info.to_index, moved_info.from_index); if(this.options.onMove) { this.options.onMove.call(this, child_node, moved_info.to_index, moved_info.from_index); } }, this); if(this.options.onIndexChange) { each(ad.index_changed, function(ic_info) { this.options.onIndexChange.call(this, ic_info.item, ic_info.to, ic_info.from); }, this); } }, function(element) { return toArray(element.childNodes); }), /** * Constrain a DOM node's CSS style * * @method cjs.bindCSS * @param {dom} element - The DOM element * @param {object} values - An object whose key-value pairs are the CSS property names and values respectively * @return {Binding} - A binding object representing the link from constraints to CSS styles * * @example If `my_elem` is a dom element * * var color = cjs('red'), * left = cjs(0); * cjs.bindCSS(my_elem, { * "background-color": color, * left: left.add('px') * }); */ /** * Constrain a DOM node's CSS style * * @method cjs.bindCSS^2 * @param {string} key - The name of the CSS attribute to constraint * @param {cjs.Constraint|string} value - The value of this CSS attribute * @return {Binding} - A binding object representing the link from constraints to elements * * @example If `my_elem` is a dom element * * var color = cjs('red'); * cjs.bindCSS(my_elem, ''background-color', color); */ css_binding = create_obj_binding(function(element, key, value) { element.style[camel_case(key)] = value; }), /** * Constrain a DOM node's attribute values * * @method cjs.bindAttr * @param {dom} element - The DOM element * @param {object} values - An object whose key-value pairs are the attribute names and values respectively * @return {Binding} - A binding object representing the link from constraints to elements * * @example If `my_elem` is an input element * * var default_txt = cjs('enter name'); * cjs.bindAttr(my_elem, 'placeholder', default_txt); */ /** * Constrain a DOM node's attribute value * * @method cjs.bindAttr^2 * @param {dom} element - The DOM element * @param {string} key - The name of the attribute to constraint * @param {cjs.Constraint|string} value - The value of this attribute * @return {Binding} - A binding object representing the link from constraints to elements * * @example If `my_elem` is an input element * * var default_txt = cjs('enter name'), * name = cjs('my_name'); * cjs.bindAttr(my_elem, { * placeholder: default_txt, * name: name * }); */ attr_binding = create_obj_binding(function(element, key, value) { if(fillAttrs[key] && !value) { // attributes like disabled that should be there or not element.removeAttribute(key); } else { element.setAttribute(key, value); } }); var inp_change_events = ["keyup", "input", "paste", "propertychange", "change"], /** * Take an input element and create a constraint whose value is constrained to the value of that input element * * @method cjs.inputValue * @param {dom} inp - The input element * @return {cjs.Constraint} - A constraint whose value is the input's value * * @example If `name_input` is an input element * * var name = cjs.inputValue(name_input), */ getInputValueConstraint = function(inps) { var arr_inp; // tracks if the input is a list of items if(isElement(inps)) { inps = [inps]; arr_inp = false; } else { arr_inp = true; } // the constraint should just return the value of the input element var constraint = cjs(function() { if(arr_inp) { return map(inps, function(inp) { return inp.value; }); // if it's an array, return every value } else { return inps[0].value; // otherwise, just return the first value } }), len = inps.length, on_change = bind(constraint.invalidate, constraint), // when any input event happens, invalidate the constraint activate = function() { // add all the event listeners for every input and event type each(inp_change_events, function(event_type) { each(inps, function(inp) { aEL(inp, event_type, on_change); }); }); }, deactivate = function() { // clear all the event listeners for every input and event type each(inp_change_events, function(event_type) { each(inps, function(inp) { rEL(inp, event_type, on_change); }); }); }, oldDestroy = constraint.destroy; // when the constraint is destroyed, remove the event listeners constraint.destroy = function() { deactivate(); oldDestroy.call(constraint); }; activate(); return constraint; }; extend(cjs, { /** @expose cjs.bindText */ bindText: text_binding, /** @expose cjs.bindHTML */ bindHTML: html_binding, /** @expose cjs.bindValue */ bindValue: val_binding, /** @expose cjs.bindChildren */ bindChildren: children_binding, /** @expose cjs.bindAttr */ bindAttr: attr_binding, /** @expose cjs.bindCSS */ bindCSS: css_binding, /** @expose cjs.bindClass */ bindClass: class_binding, /** @expose cjs.inputValue */ inputValue: getInputValueConstraint, /** @expose cjs.Binding */ Binding: Binding }); // Finite State Machines // --------------------- // State keeps track of basic state information (its containing FSM does most of the work) var State = function(fsm, name) { this._fsm = fsm; // parent fsm this._name = name; // state name (fetch with getName) this._id = uniqueId(); // useful for storage }; (function(my) { var proto = my.prototype; proto.getName = function() { return this._name; }; // getter for name proto.id = function() { return this._id; }; // getter for id }(State)); // Simple transition representation (again, the containing FSM does most of the work) var Transition = function(fsm, from_state, to_state, name) { this._fsm = fsm; // parent FSM this._from = from_state; // from state (fetch with getFrom) this._to = to_state; // to state (fetch with getTo) this._name = name; // name (fetch with getName) this._id = uniqueId(); // useful for storage this._event = false; // the CJSEvent (if created) for this transition }; (function(my) { var proto = my.prototype; proto.getFrom = function() { return this._from; }; // from getter proto.getTo = function() { return this._to; }; // to getter proto.getName = function() { return this._name; }; // name getter proto.getFSM = function() { return this._fsm; }; // FSM getter proto.id = function() { return this._id; }; // getter for id proto.destroy = function() { var ev = this._event; if(ev) { ev._removeTransition(this); } delete this._event; delete this._fsm; delete this._from; delete this._to; }; proto.setEvent = function(event) { this._event = event; }; proto.run = function() { var fsm = this.getFSM(); // do_transition should be called by the user's code if(fsm && fsm.is(this.getFrom())) { var args = toArray(arguments); args.unshift(this.getTo(), this); fsm._setState.apply(fsm, args); } }; }(Transition)); /* * The following selector constructors are used internally to keep track of user-specified * selectors (a -> b represents the transition from a to b). * * Developers using cjs can specify that they want to add listeners for any number of such * selectors and they will be dynamically evaluated and called. For instance, if the user * adds a selector for any state to stateA (represented as * -> stateA) *before* stateA is * created, then if the developer later adds a state named stateA, their callback should be * called whenever the fsm transitions to that newly created stateA */ // The selector for a state with a supplied name (e.g. stateA) var StateSelector = function(state_name) { this._state_name = state_name; }; (function(my) { var proto = my.prototype; proto.matches = function(state) { // Supplied object should be a State object with the given name return this._state_name === state || (state instanceof State && this._state_name === state.getName()); }; }(StateSelector)); // Matches any state (e.g. *) var AnyStateSelector = function() { }; (function(my) { var proto = my.prototype; // will match any state (but not transition) // Checking if it isn't a transition (rather than if it is a State) because sometimes, this is // checked against state *names* rather than the state itself proto.matches = function(state) { return !(state instanceof Transition);}; }(AnyStateSelector)); // Matches certain transitions (see transition formatting spec) var TransitionSelector = function(pre, from_state_selector, to_state_selector) { this.is_pre = pre; // should fire before the transition (as opposed to after) this.from_state_selector = from_state_selector; // the selector for the from state (should be a StateSelector or AnyStateSelector) this.to_state_selector = to_state_selector; // selector for the to state }; (function(my) { var proto = my.prototype; // Make sure that the supplied object is a transition with the same timing proto.matches = function(transition, pre) { if(transition instanceof Transition && this.is_pre === pre) { var from_state = transition.getFrom(); var to_state = transition.getTo(); // And then make sure both of the states match as well return this.from_state_selector.matches(from_state) && this.to_state_selector.matches(to_state); } else { return false; } }; }(TransitionSelector)); // Multiple possibilities (read OR, not AND) var MultiSelector = function() { this.selectors = toArray(arguments); // all of the selectors to test }; (function(my) { var proto = my.prototype; proto.matches = function() { var match_args = arguments; // See if any selectors match return any(this.selectors, function(selector) { return selector.matches.apply(selector, match_args); }); }; }(MultiSelector)); // return a selector object from a string representing a single state var parse_single_state_spec = function(str) { if(str === "*") { return new AnyStateSelector(); } else { return new StateSelector(str); } }; // Parse one side of the transition var parse_state_spec = function(str) { // Split by , and remove any excess spacing var state_spec_strs = map(str.split(","), function(ss) { return trim(ss); }); // The user only specified one state if(state_spec_strs.length === 1) { return parse_single_state_spec(state_spec_strs[0]); } else { // any number of states var state_specs = map(state_spec_strs, parse_single_state_spec); return new MultiSelector(state_specs); } }; // The user specified a transition var parse_transition_spec = function(left_str, transition_str, right_str) { var left_to_right_transition, right_to_left_transition; var left_state_spec = parse_state_spec(left_str); var right_state_spec = parse_state_spec(right_str); // Bi-directional, after transition if(transition_str === "<->") { left_to_right_transition = new TransitionSelector(false, left_state_spec, right_state_spec); right_to_left_transition = new TransitionSelector(false, right_state_spec, left_state_spec); return new MultiSelector(left_to_right_transition, right_to_left_transition); } else if(transition_str === ">-<") { // bi-directional, before transition left_to_right_transition = new TransitionSelector(true, left_state_spec, right_state_spec); right_to_left_transition = new TransitionSelector(true, right_state_spec, left_state_spec); return new MultiSelector(left_to_right_transition, right_to_left_transition); } else if(transition_str === "->") { // left to right, after transition return new TransitionSelector(false, left_state_spec, right_state_spec); } else if(transition_str === ">-") { // left to right, before transition return new TransitionSelector(true, left_state_spec, right_state_spec); } else if(transition_str === "<-") { // right to left, after transition return new TransitionSelector(false, right_state_spec, left_state_spec); } else if(transition_str === "-<") { // right to left, before transition return new TransitionSelector(true, right_state_spec, left_state_spec); } else { return null; } // There shouldn't be any way to get here... }; var transition_separator_regex = /^([\sa-zA-Z0-9,\-_*]+)((<->|>-<|->|>-|<-|-<)([\sa-zA-Z0-9,\-_*]+))?$/; // Given a string specifying a state or set of states, return a selector object var parse_spec = function(str) { var matches = str.match(transition_separator_regex); if(matches === null) { return null; // Poorly formatted specification } else { if(matches[2]) { // The user specified a transition: "A->b": ["A->b", "A", "->b", "->", "b"] var from_state_str = matches[1], transition_str = matches[3], to_state_str = matches[4]; return parse_transition_spec(from_state_str, transition_str, to_state_str); } else { // The user specified a state: "A": ["A", "A", undefined, undefined, undefined] var states_str = matches[1]; return parse_state_spec(states_str); } } }; // StateListener var state_listener_id = 0; var StateListener = function(selector, callback, context) { this._context = context || root; // 'this' in the callback this._selector = selector; // used to record interest this._callback = callback; // the function to call when selector matches this._id = state_listener_id++; // unique id }; (function(my) { var proto = my.prototype; // Used to determine if run should be called by the fsm proto.interested_in = function() { return this._selector.matches.apply(this._selector, arguments); }; // Run the user-specified callback proto.run = function() { this._callback.apply(this._context, arguments); }; }(StateListener)); /** * ***Note:*** The preferred way to create a FSM is through the `cjs.fsm` function * This class represents a finite-state machine to track the state of an interface or component * * @private * @class cjs.FSM * @classdesc A finite-state machine * @param {string} ...state_names - Any number of state names for the FSM to have * @see cjs.fsm */ var FSM = function() { this._states = {}; // simple substate representations this._transitions = []; // simple transition representations this._curr_state = null; // the currently active state this._listeners = []; // listeners for every selector this._chain_state = null; // used internally for chaining this._did_transition = false; // keeps track of if any transition has run (so that when the user specifies // a start state, it knows whether or not to change the current state /** * The name of this FSM's active state * @property {Constraint} cjs.FSM.state * @example * * var my_fsm = cjs.fsm("state1", "state2"); * my_fsm.state.get(); // 'state1' */ this.state = cjs(function() { // the name of the current state if(this._curr_state) { return this._curr_state._name; } else { return null; } }, { context: this }); // Option to pass in state names as arguments this.addState.apply(this, flatten(arguments, true)); }; (function(my) { var proto = my.prototype; /** @lends cjs.FSM.prototype */ // Find the state with a given name var getStateWithName = function(fsm, state_name) { return fsm._states[state_name]; }; /** * Create states and set the current "chain state" to that state * * @method addState * @param {string} ...state_names - Any number of state names to add. The last state becomes the chain state * @return {FSM} - `this` * * @example * * var fsm = cjs.fsm() * .addState('state1') * .addState('state2') * .addTransition('state2', cjs.on('click')); */ proto.addState = function() { var state; each(arguments, function(state_name) { state = getStateWithName(this, state_name); if(!state) { state = this._states[state_name] = new State(this, state_name); // if there isn't an active state, // make this one the starting state by default if(this._curr_state === null) { this._curr_state = state; } } }, this); if(state) { this._chain_state = state; } return this; }; /** * Returns the name of the state this machine is currently in. Constraints that depend on the return * value will be automatically updated. * * @method getState * @return {string} - The name of the currently active state * @example * * var my_fsm = cjs.fsm("state1", "state2"); * my_fsm.getState(); // 'state1' */ proto.getState = function() { return this.state.get(); }; /** * Add a transition between two states * * @method addTransition * @param {string} to_state - The name of the state the transition should go to * @return {function} - A function that tells the transition to run * @example * * var x = cjs.fsm(); * x.addState("b") * .addState("a"); * var run_transition = x.addTransition("b"); * //add a transition from a to b * window.addEventListener("click", run_transition); * // run that transition when the window is clicked */ /** * (variant 2) * @method addTransition^2 * @param {string} to_state - The name of the state the transition should go to * @param {CJSEvent|function} add_transition_fn - A `CJSEvent` or a user-specified function for adding the event listener * @return {FSM} - `this` * @example * * var x = cjs.fsm(); * x.addState("b") * .addState("a") * .addTransition("b", cjs.on('click')); * // add a transition from a to b that runs when the window is clicked * @example * * var x = cjs.fsm(); * x.addState("b") * .addState("a") * .addTransition("b", function(run_transition) { * window.addEventListener("click", run_transition); * }); * // add a transition from a to b that runs when the window is clicked */ /** * (variant 3) * @method addTransition^3 * @param {string} from_state - The name of the state the transition should come from * @param {string} to_state - The name of the state the transition should go to * @return {function} - A function that tells the transition to run * @example * * var x = cjs.fsm("a", "b"); * var run_transition = x.addTransition("a", "b"); //add a transition from a to b * window.addEventListener("click", run_transition); // run that transition when the window is clicked */ /** * (variant 4) * @method addTransition^4 * @param {string} from_state - The name of the state the transition should come from * @param {string} to_state - The name of the state the transition should go to * @param {CJSEvent|function} add_transition_fn - A `CJSEvent` or a user-specified function for adding the event listener * @return {FSM} - `this` * * @example * * var x = cjs.fsm("a", "b"); * x.addTransition("a", "b", cjs.on("click")); * @example * * var x = cjs.fsm("a", "b"); * var run_transition = x.addTransition("a", "b", function(run_transition) { * window.addEventListener("click", run_transition); * }); // add a transition from a to b that runs when the window is clicked */ proto.addTransition = function(a, b, c) { var from_state, to_state, transition, add_transition_fn, return_transition_func = false; if(arguments.length === 0) { throw new Error("addTransition expects at least one argument"); } else if(arguments.length === 1) { // make a transition from the last entered state to the next state return_transition_func = true; from_state = this._chain_state; to_state = a; } else if(arguments.length === 2) { if(isFunction(b) || b instanceof CJSEvent) { // b is the function to add the transition from_state = this._chain_state; to_state = a; add_transition_fn = b; } else { // from and to states specified from_state = a; to_state = b; return_transition_func = true; } } else { from_state = a; to_state = b; add_transition_fn = c; } if(isString(from_state) && !has(this._states, from_state)) { this._states[from_state] = new State(this, from_state); } if(isString(to_state) && !has(this._states, to_state)) { this._states[to_state] = new State(this, to_state); } // do_transition is a function that can be called to activate the transition // Creates a new transition that will go from from_state to to_state transition = new Transition(this, from_state, to_state); this._transitions.push(transition); if(return_transition_func) { return bind(transition.run, transition); } else { if(add_transition_fn instanceof CJSEvent) { add_transition_fn._addTransition(transition); transition.setEvent(add_transition_fn); } else { // call the supplied function with the code to actually perform the transition add_transition_fn.call(this, bind(transition.run, transition), this); } return this; } }; /** * Changes the active state of this FSM. * This function should, ideally, be called by a transition instead of directly. * * @private * @method _setState * @param {State|string} state - The state to transition to * @param {Transition} transition - The transition that ran */ proto._setState = function(state, transition, event) { var from_state = this.getState(), // the name of my current state to_state = isString(state) ? getStateWithName(this, state) : state, listener_args = this._listeners.length > 0 ? ([event, transition, to_state, from_state]).concat(rest(arguments, 3)) : false; if(!to_state) { throw new Error("Could not find state '" + state + "'"); } this.did_transition = true; // Look for pre-transition callbacks each(this._listeners, function(listener) { if(listener.interested_in(transition, true)) { listener.run.apply(listener, listener_args); // and run 'em } }); this._curr_state = to_state; this.state.invalidate(); // Look for post-transition callbacks.. // and also callbacks that are interested in state entrance each(this._listeners, function(listener) { if(listener.interested_in(transition, false) || listener.interested_in(to_state)) { listener.run.apply(listener, listener_args); // and run 'em } }); }; /** * Remove all of the states and transitions of this FSM. Useful for cleaning up memory * * @method destroy */ proto.destroy = function() { this.state.destroy(); this._states = {}; each(this._transitions, function(t) { t.destroy(); }); this._transitions = []; this._curr_state = null; }; /** * Specify which state this FSM should begin at. * * @method startsAt * @param {string} state_name - The name of the state to start at * @return {FSM} - `this` * @example * * var my_fsm = cjs.fsm("state_a", "state_b"); * my_fsm.startsAt("state_b"); */ proto.startsAt = function(state_name) { var state = getStateWithName(this, state_name); // Get existing state if(!state) { // or create it if necessary state = this._states[state_name] = new State(this, state_name); } if(!this.did_transition) { // If no transitions have occurred, set the current state to the one they specified this._curr_state = state; this.state.invalidate(); } this._chain_state = state; return this; }; /** * Check if the current state is `state_name` * * @method is * @param {string} state_name - The name of the state to check against * @return {boolean} - `true` if the name of the active state is `state_name`. `false` otherwise * @example * * var my_fsm = cjs.fsm("a", "b"); * my_fsm.is("a"); // true, because a is the starting state */ proto.is = function(state_name) { // get the current state name & compare var state = this.getState(); return state === null ? false : (state === (isString(state_name) ? state_name : state_name.getName())); }; /** * Call a given function when the finite-state machine enters a given state. * `spec` can be of the form: * - `'*'`: any state * - `'state1'`: A state named `state1` * - `'state1 -> state2'`: Immediately **after** state1 transitions to state2 * - `'state1 >- state2'`: Immediately **before** state1 transitions to state2 * - `'state1 <-> state2'`: Immediately **after** any transition between state1 and state2 * - `'state1 >-< state2'`: Immediately **before** any transition between state1 and state2 * - `'state1 <- state2'`: Immediately **after** state2 transitions 2 state1 * - `'state1 -< state2'`: Immediately **before** state2 transitions 2 state1 * - `'state1 -> *'`: Any transition from state1 * - `'* -> state2'`: Any transition to state2 * * @method on * @param {string} spec - A specification of which state to call the callback * @param {function} callback - The function to be called * @param {object} [context] - What `this` should evaluate to when `callback` is called * @return {FSM} - `this` * * @see FSM.prototype.off * @example * * var x = cjs.fsm("a", "b"); * x.on("a->b", function() {...}); */ proto.on = proto.addEventListener = function(spec_str, callback, context) { var selector; if(isString(spec_str)) { selector = parse_spec(spec_str); if(selector === null) { throw new Error("Unrecognized format for state/transition spec."); } } else { selector = spec_str; } var listener = new StateListener(selector, callback, context); this._listeners.push(listener); return this; }; /** * Remove the listener specified by an on call; pass in just the callback * * @method off * @param {function} callback - The function to remove as a callback * @return {FSM} - `this` * * @see FSM.prototype.on */ proto.off = proto.removeEventListener = function(listener_callback) { this._listeners = filter(this._listeners, function(listener) { return listener.callback !== listener_callback; }); return this; }; }(FSM)); /** @lends */ extend(cjs, { /** @expose cjs.FSM */ FSM: FSM, /** * Create an FSM * @method cjs.fsm * @constructs FSM * @param {string} ...state_names - An initial set of state names to add to the FSM * @return {FSM} - A new FSM * @see FSM * @example Creating a state machine with two states * * var my_state = cjs.fsm("state1", "state2"); */ fsm: function() { return new FSM(arguments); }, /** * Determine whether an object is an FSM * @method cjs.isFSM * @param {*} obj - An object to check * @return {boolean} - `true` if `obj` is an `FSM`, `false` otherwise */ isFSM: function(obj) { return obj instanceof FSM; } }); var CJSEvent = function(parent, filter, onAddTransition, onRemoveTransition) { this._listeners = []; // parent events that want to know when I fire this._transitions = []; // a list of transitions that I'm attached to this._on_add_transition = onAddTransition; // optional listener for when a transition is added this._on_remove_transition = onRemoveTransition; // optional listener for when a transition is removed this._live_fns = {}; // one per transitions this._parent = parent; if(this._parent) { this._parent._listeners.push({event:this, filter: filter}); // add an item to my parent's listener if i have a parent } }; (function(my) { /** @lends cjs.CJSEvent.prototype */ var proto = my.prototype; /** * Create a transition that calls filter whenever it fires to ensure that it should fire * * @method guard * @param {function} [filter] - Returns `true` if the event should fire and false otherwise * @return {CJSEvent} A new event that only fires when `filter` returns a truthy value * @example If the user clicks and `ready` is `true` * * cjs.on("click").guard(function() { * return ready === true; * }); */ proto.guard = function(filter, filter_eq) { //Assume filter is the name of a paroperty if(!isFunction(filter)) { var prop_name = filter; filter = function(event) { return event && event[prop_name] === filter_eq; }; } return new CJSEvent(this, filter); }; /** * Add a transition to my list of transitions that this event is attached to * * @private * @method _addTransition * @param {Transition} transition - The transition this event is attached to */ proto._addTransition = function(transition) { this._transitions.push(transition); if(this._on_add_transition) { this._live_fns[transition.id()] = this._on_add_transition(transition); } if(this._parent && this._parent._on_add_transition) { this._parent._on_add_transition(transition); } }; /** * Remove a transition from my list of transitions * * @private * @method _removeTransition * @param {Transition} transition - The transition this event is attached to */ proto._removeTransition = function(transition) { if(remove(this._transitions, transition)) { if(this._on_remove_transition) { this._on_remove_transition(transition); // clear the live fn var tid = transition.id(); this._live_fns[tid].destroy(); delete this._live_fns[tid]; } } if(this._parent && this._parent._on_remove_transition) { this._parent._on_remove_transition(transition); } }; /** * When I fire, go through every transition I'm attached to and fire it then let any interested listeners know as well * * @private * @method _fire * @param {*} ...events - Any number of events that will be passed to the transition */ proto._fire = function() { var events = arguments; each(this._transitions, function(transition) { transition.run.apply(transition, events); }); each(this._listeners, function(listener_info) { var listener = listener_info.event, filter = listener_info.filter; if(!filter || filter.apply(root, events)) { listener._fire.apply(listener, events); } }); }; }(CJSEvent)); /** @lends */ var isElementOrWindow = function(elem) { return elem === root || isPolyDOM(elem); }, split_and_trim = function(x) { return map(x.split(" "), trim); }, timeout_event_type = "timeout"; extend(cjs, { /** @expose cjs.CJSEvent */ CJSEvent: CJSEvent, /** * Create a new event for use in a finite state machine transition * * @constructs CJSEvent * @method cjs.on * @param {string} event_type - the type of event to listen for (e.g. mousedown, timeout) * @param {element|number} ...targets=window - Any number of target objects to listen to * @return {CJSEvent} - An event that can be attached to * @example When the window resizes * * cjs.on("resize") * * @example When the user clicks `elem1` or `elem2` * * cjs.on("click", elem1, elem2) * * @example After 3 seconds * * cjs.on("timeout", 3000) */ on: function(event_type) { var rest_args = arguments.length > 1 ? rest(arguments) : root, // no parent, no filter by default event = new CJSEvent(false, false, function(transition) { var targets = [], timeout_id = false, event_type_val = [], listener = bind(this._fire, this), fsm = transition.getFSM(), from = transition.getFrom(), state_selector = new StateSelector(from), from_state_selector = new TransitionSelector(true, state_selector, new AnyStateSelector()), on_listener = function() { each(event_type_val, function(event_type) { // If the event is 'timeout' if(event_type === timeout_event_type) { // clear the previous timeout if(timeout_id) { cTO(timeout_id); timeout_id = false; } // and set a new one var delay = cjs.get(rest_args[0]); if(!isNumber(delay) || delay < 0) { delay = 0; } timeout_id = sTO(listener, delay); } else { each(targets, function(target) { // otherwise, add the event listener to every one of my targets aEL(target, event_type, listener); }); } }); }, off_listener = function() { each(event_type_val, function(event_type) { each(targets, function(target) { if(event_type === timeout_event_type) { // If the event is 'timeout' if(timeout_id) { cTO(timeout_id); timeout_id = false; } } else { rEL(target, event_type, listener); } }); }); }, live_fn = cjs.liven(function() { off_listener(); event_type_val = split_and_trim(cjs.get(event_type)); // only use DOM elements (or the window) as my target targets = flatten(map(filter(get_dom_array(rest_args), isElementOrWindow), getDOMChildren , true)); // when entering the state, add the event listeners, then remove them when leaving the state fsm .on(state_selector, on_listener) .on(from_state_selector, off_listener); if(fsm.is(from)) { // if the FSM is already in the transition's starting state on_listener(); } }); return live_fn; }); return event; } }); // Based on [Mu's parser](https://github.com/raycmorgan/Mu) and // John Resig's [HTML parser](http://erik.eae.net/simplehtmlparser/simplehtmlparser.js) var makeMap = function(str){ var obj = {}; each(str.split(","), function(item) { obj[item] = true; }); return obj; }; // Regular Expressions for parsing tags and attributes var startTag = /^<([\-A-Za-z0-9_]+)((?:\s+[a-zA-Z0-9_\-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^>\s]+)))?)*)\s*(\/?)>/, endTag = /^<\/([\-A-Za-z0-9_]+)[^>]*>/, handlebar = /^\{\{([#=!>|{\/])?\s*((?:(?:"[^"]*")|(?:'[^']*')|[^\}])*)\s*(\/?)\}?\}\}/, attr = /([\-A-Za-z0-9_]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^\/>\s]+)))?/g, //hb_attr = /\{\{([^\}]*)\}\}/g, HB_TYPE = "hb", HTML_TYPE = "html"; // Empty Elements - HTML 4.01 var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed"), // Block Elements - HTML 4.01 block = makeMap("address,applet,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,form,frameset,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul"), // Inline Elements - HTML 4.01 inline = makeMap("a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var"), // Elements that you can, intentionally, leave open (and which close themselves) closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr"), // Attributes that have their values filled in disabled="disabled" fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected"), // Special Elements (can contain anything) special = makeMap("script,style"); var IF_TAG = "if", ELIF_TAG = "elif", ELSE_TAG = "else", STATE_TAG = "state", EACH_TAG = "each", WITH_TAG = "with", FSM_TAG = "fsm", UNLESS_TAG= "unless"; // Dictates what parents children must have; state must be a direct descendent of diagram var parent_rules = {}; parent_rules[STATE_TAG] = { parent: [FSM_TAG] }; parent_rules[ELIF_TAG] = { parent: [IF_TAG] }; parent_rules[ELSE_TAG] = { parent: [IF_TAG, EACH_TAG] }; var autoclose_nodes = {}; autoclose_nodes[ELIF_TAG] = { when_open_sibling: [ELIF_TAG, ELSE_TAG] }; autoclose_nodes[ELSE_TAG] = { when_close_parent: [IF_TAG, EACH_TAG], when_open_sibling: [] }; autoclose_nodes[STATE_TAG] = { when_open_sibling: [STATE_TAG] }; // elsif and else must come after either if or elsif var sibling_rules = {}; sibling_rules[ELIF_TAG] = { follows: [ELIF_TAG], //what it may follow or_parent: [IF_TAG] //or the parent can be 'if' }; sibling_rules[ELSE_TAG] = { follows: [ELIF_TAG], or_parent: [IF_TAG, EACH_TAG] }; sibling_rules[STATE_TAG] = { follows: [STATE_TAG], or_parent: [FSM_TAG] }; var parseTemplate = function(input_str, handler) { var html_index, hb_index, last_closed_hb_tag, index, chars, match, stack = [], last = input_str; stack.last = function() { return this[this.length - 1]; }; var replace_fn = function(all, text) { text = text .replace(//g, "$1") .replace(//g, "$1"); if (handler.chars) { handler.chars(text); } return ""; }; while (input_str) { chars = true; // Make sure we're not in a script or style element if (!stack.last() || !special[stack.last()]) { // Comment if (input_str.indexOf(""); if (index >= 0) { if (handler.HTMLcomment) { handler.HTMLcomment( input_str.substring( 4, index ) ); } input_str = input_str.substring( index + 3 ); chars = false; } // end tag } else if (input_str.indexOf("]*>"), replace_fn); parseEndTag("", stack.last()); } if (input_str == last) { throw new Error("Parse Error: " + input_str); } last = input_str; } // Clean up any remaining tags parseEndTag(); function parseStartTag( tag, tagName, rest, unary ) { tagName = tagName.toLowerCase(); if ( block[ tagName ] ) { while ( stack.last() && inline[ stack.last() ] ) { parseEndTag( "", stack.last() ); } } if ( closeSelf[ tagName ] && stack.last() == tagName ) { parseEndTag( "", tagName ); } unary = empty[ tagName ] || !!unary; if ( !unary ) { stack.push({type: HTML_TYPE, tag: tagName}); } if (handler.startHTML) { var attrs = []; rest.replace(attr, function(match, name) { var value = arguments[2] ? arguments[2] : arguments[3] ? arguments[3] : arguments[4] ? arguments[4] : fillAttrs[name] ? name : ""; attrs.push({ name: name, value: value, escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //" }); }); handler.startHTML(tagName, attrs, unary); } } function parseEndTag(tag, tagName) { popStackUntilTag(tagName, HTML_TYPE); } function getLatestHandlebarParent() { var i, stack_i; for(i = stack.length - 1; i>= 0; i--) { stack_i = stack[i]; if(stack_i.type === HB_TYPE) { return stack_i; } } return undefined; } function parseHandlebar(tag, prefix, content) { var last_stack, tagName, parsed_content = jsep(content); if(parsed_content.type === COMPOUND) { if(parsed_content.body.length > 0 && parsed_content.body[0].type === IDENTIFIER) { tagName = parsed_content.body[0].name; } } else { if(parsed_content.type === IDENTIFIER) { tagName = parsed_content.name; } } switch (prefix) { case '{': // literal handler.startHB(tagName, parsed_content, true, true); break; case '>': // partial handler.partialHB(tagName, parsed_content); break; case '#': // start block last_stack = getLatestHandlebarParent(); if(last_stack && has(autoclose_nodes, last_stack.tag)) { var autoclose_node = autoclose_nodes[last_stack.tag]; if(indexOf(autoclose_node.when_open_sibling, tagName) >= 0) { popStackUntilTag(last_stack.tag, HB_TYPE); last_stack = getLatestHandlebarParent(); } } if(has(parent_rules, tagName)) { var parent_rule = parent_rules[tagName]; if(!last_stack || indexOf(parent_rule.parent, last_stack.tag)<0) { throw new Error("'" + tagName + "' must be inside of a '"+parent_rule.parent+"' block"); } } if(has(sibling_rules, tagName)) { var sibling_rule = sibling_rules[tagName]; if(indexOf(sibling_rule.follows, last_closed_hb_tag) < 0) { if(!sibling_rule.or_parent || indexOf(sibling_rule.or_parent, last_stack.tag) < 0) { var error_message = "'" + tagName + "' must follow a '" + sibling_rule.follows[0] + "'"; if(sibling_rule.or_parent) { error_message += " or be inside of a '" + sibling_rule.or_parent[0] + "' tag"; } throw new Error(error_message); } } } stack.push({type: HB_TYPE, tag: tagName}); handler.startHB(tagName, parsed_content, false); break; case '/': // end block popStackUntilTag(tagName, HB_TYPE); break; case '!': // end block break; default: // unary handler.startHB(tagName, parsed_content, true, false); break; } } function popStackUntilTag(tagName, type) { var i, pos, stack_i; for (pos = stack.length - 1; pos >= 0; pos -= 1) { if(stack[pos].type === type && stack[pos].tag === tagName) { break; } } if (pos >= 0) { // Close all the open elements, up the stack for (i = stack.length - 1; i >= pos; i-- ) { stack_i = stack[i]; if(stack_i.type === HB_TYPE) { if (handler.endHB) { handler.endHB(stack_i.tag); } } else { if (handler.endHTML) { handler.endHTML(stack_i.tag); } } } // Remove the open elements from the stack stack.length = pos; } if(type === HB_TYPE) { last_closed_hb_tag = tagName; } } }, create_template = function(template_str) { var root = { children: [], type: ROOT_TYPE }, stack = [root], last_pop = false, has_container = false, fsm_stack = [], condition_stack = []; parseTemplate(template_str, { startHTML: function(tag, attributes, unary) { last_pop = { type: HTML_TYPE, tag: tag, attributes: attributes, unary: unary, children: [] }; last(stack).children.push(last_pop); if(!unary) { stack.push(last_pop); } }, endHTML: function(tag) { last_pop = stack.pop(); }, HTMLcomment: function(str) { last_pop = { type: COMMENT_TYPE, str: str }; last(stack).children.push(last_pop); }, chars: function(str) { last_pop = { type: CHARS_TYPE, str: str }; last(stack).children.push(last_pop); }, startHB: function(tag, parsed_content, unary, literal) { if(unary) { last_pop = { type: UNARY_HB_TYPE, obj: first_body(parsed_content), literal: literal, //options: body_event_options(parsed_content), tag: tag }; last(stack).children.push(last_pop); } else { var push_onto_children = true; last_pop = { type: HB_TYPE, tag: tag, children: [] }; switch(tag) { case EACH_TAG: last_pop.parsed_content = rest_body(parsed_content); last_pop.else_child = false; break; case UNLESS_TAG: case IF_TAG: last_pop.reverse = tag === UNLESS_TAG; last_pop.sub_conditions = []; last_pop.condition = rest_body(parsed_content); condition_stack.push(last_pop); break; case ELIF_TAG: case ELSE_TAG: var last_stack = last(stack); if(last_stack.type === HB_TYPE && last_stack.tag === EACH_TAG) { last_stack.else_child = last_pop; } else { last(condition_stack).sub_conditions.push(last_pop); } last_pop.condition = tag === ELSE_TAG ? ELSE_COND : rest_body(parsed_content); push_onto_children = false; break; case EACH_TAG: case FSM_TAG: last_pop.fsm_target = rest_body(parsed_content); last_pop.sub_states = {}; fsm_stack.push(last_pop); break; case STATE_TAG: var state_name = parsed_content.body[1].name; last(fsm_stack).sub_states[state_name] = last_pop; push_onto_children = false; break; case WITH_TAG: last_pop.content = rest_body(parsed_content); break; } if(push_onto_children) { last(stack).children.push(last_pop); } stack.push(last_pop); } }, endHB: function(tag) { switch(tag) { case IF_TAG: case UNLESS_TAG: condition_stack.pop(); break; case FSM_TAG: fsm_stack.pop(); } stack.pop(); }, partialHB: function(tagName, parsed_content) { last_pop = { type: PARTIAL_HB_TYPE, tag: tagName, content: rest_body(parsed_content) }; last(stack).children.push(last_pop); } }); return root; }; var child_is_dynamic_html = function(child) { return child.type === UNARY_HB_TYPE && child.literal; }, child_is_text = function(child) { return child.isText; }, every_child_is_text = function(arr) { return every(arr, child_is_text); }, any_child_is_dynamic_html = function(arr) { return any(arr, child_is_dynamic_html); }, PARTIAL_HB_TYPE = "partial_hb", UNARY_HB_TYPE = "unary_hb", CHARS_TYPE = "chars", ROOT_TYPE = "root", COMMENT_TYPE = "comment", TEMPLATE_INSTANCE_PROP = "data-cjs-template-instance", outerHTML = function (node){ // if IE, Chrome take the internal method otherwise build one return node.outerHTML || ( function(n){ var div = document.createElement('div'), h; div.appendChild( n.cloneNode(true) ); h = div.innerHTML; div = null; return h; })(node); }, escapeHTML = function (unsafe) { return unsafe .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }, compute_object_property = function(object, prop_node, context, lineage) { if(object) { var nodeName = prop_node.computed ? get_node_value(prop_node, context, lineage) : prop_node.name; if(cjs.isMapConstraint(object)) { return object.get(nodeName); } else { return object[nodeName]; } } else { return undefined; } }, ELSE_COND = {}, first_body = function(node) { return node.type === COMPOUND ? node.body[0] : node; }, rest_body = function(node) { return {type: COMPOUND, body: node.type === COMPOUND ? rest(node.body) : [] }; }, get_instance_nodes = function(c) { return c.node || c.getNodes(); }, get_node_value = function(node, context, lineage) { var op, object, call_context, args, val, name, i; if(!node) { return; } switch(node.type) { case THIS_EXP: return cjs.get(last(lineage).this_exp); case LITERAL: return node.value; case UNARY_EXP: op = unary_operators[node.operator]; return op ? op(get_node_value(node.argument, context, lineage)) : undefined; case BINARY_EXP: case LOGICAL_EXP: op = binary_operators[node.operator]; return op ? op(get_node_value(node.left, context, lineage), get_node_value(node.right, context, lineage)) : undefined; case IDENTIFIER: if(node.name.charAt(0) === "@") { name = node.name.slice(1); for(i = lineage.length-1; i>=0; i--) { object = lineage[i].at; if(object && has(object, name)) { val = object[name]; break; } } } else { if(cjs.isMapConstraint(context)) { val = context.get(node.name); } else { val = context[node.name]; } } return is_constraint(val) ? val.get() : val; case MEMBER_EXP: object = get_node_value(node.object, context, lineage); return compute_object_property(object, node.property, context, lineage); case COMPOUND: return get_node_value(node.body[0], context, lineage); case CURR_LEVEL_EXP: object = last(lineage).this_exp; return compute_object_property(object, node.argument, context, lineage); case PARENT_EXP: object = (lineage && lineage.length > 1) ? lineage[lineage.length - 2].this_exp : undefined; return compute_object_property(object, node.argument, context, lineage); case CONDITIONAL_EXP: return get_node_value(node.test, context, lineage) ? get_node_value(node.consequent, context, lineage) : get_node_value(node.alternate, context, lineage); case CALL_EXP: if(node.callee.type === MEMBER_EXP) { call_context = get_node_value(node.callee.object, context, lineage); object = compute_object_property(call_context, node.callee.property, context, lineage); } else { call_context = root; object = get_node_value(node.callee, context, lineage); } if(object && isFunction(object)) { args = map(node['arguments'], function(arg) { return get_node_value(arg, context, lineage); }); return object.apply(call_context, args); } } }, get_escaped_html = function(c) { if(c.nodeType === 3) { return escapeHTML(getTextContent(c)); } else { return escapeHTML(outerHTML(c)); } }, get_concatenated_inner_html_constraint = function(children, context, lineage) { var args = arguments; return cjs(function() { return map(children, function(child) { if(child.type === UNARY_HB_TYPE) { if(child.literal) { return get_node_value(child.val, context, lineage); } else { return escapeHTML(get_node_value(child.val, context, lineage)+""); } } else { var child_val = get_instance_nodes(child); if(isArray(child_val)) { return map(child_val, get_escaped_html).join(""); } else { return get_escaped_html(child_val); } } }).join(""); }); }, get_concatenated_children_constraint = function(children, args) { return cjs(function() { var rv = []; each(children, function(child) { var c_plural = get_instance_nodes(child); if(isArray(c_plural)) { rv.push.apply(rv, c_plural); } else { rv.push(c_plural); } }); return rv; }); }, hb_regex = /^\{\{([^\}]+)\}\}/, get_constraint = function(str, context, lineage) { var has_constraint = false, has_str = false, strs = [], index, match_val, len = 0, substr, last_val_is_str = false; while(str.length > 0) { index = str.indexOf("{"); if(index === 0) { match_val = str.match(hb_regex); if(match_val) { strs[len++] = cjs(bindArgs(get_node_value, jsep(match_val[1]), context, lineage)); str = str.substr(match_val[0].length); last_val_is_str = false; has_constraint = true; continue; } else { // !match_val index++; // capture this '{' in index } } if(index < 0) { index = str.length; } substr = str.substr(0, index); str = str.substr(index); if(last_val_is_str) { strs[len-1] = strs[len-1] + substr; } else { strs[len++] = substr; } has_str = last_val_is_str = true; } if(has_constraint) { return (!has_str && strs.length===1) ? strs[0] : cjs(function() { return map(strs, function(str) { if(is_constraint(str)) { return str.get(); } else if(is_array(str)) { return str.join(" "); } else { return "" + str; } }).join(""); }); } else { return strs.join(""); } }, array_this_eq = function(a, b) { return a.length === b.length && every(a, function(ai, i) { return ai.this_esp === b[i].this_esp; }); }, IS_OBJ = {}, map_aware_array_eq = function(a, b) { return a === b || (a && a.is_obj === IS_OBJ && a.key === b.key && a.value === b.value); }, name_regex = /^(data-)?cjs-out$/, on_regex = /^(data-)?cjs-on-(\w+)$/, call_each = function(arr, prop_name) { var args = rest(arguments, 2); each(arr, function(x) { if(has(x, prop_name)) { x[prop_name].apply(x, args); } }); }, pause_each = function(arr) { call_each.apply(this, ([arr, "pause"]).concat(rest(arguments))); }, resume_each = function(arr) { call_each.apply(this, ([arr, "resume"]).concat(rest(arguments))); }, destroy_each = function(arr) { call_each.apply(this, ([arr, "destroy"]).concat(rest(arguments))); }, onadd_each = function(arr) { call_each.apply(this, ([arr, "onAdd"]).concat(rest(arguments))); }, onremove_each = function(arr) { call_each.apply(this, ([arr, "onRemove"]).concat(rest(arguments))); }, create_template_instance = function(template, context, lineage, parent_dom_node) { var type = template.type, instance_children, element, active_children; if(type === CHARS_TYPE) { return {type: type, node: doc.createTextNode(template.str) }; } else if(type === ROOT_TYPE || type === HTML_TYPE) { var args = arguments, on_regex_match, bindings = [], binding; instance_children = map(template.children, function(child) { return create_template_instance(child, context, lineage); }); if(type === ROOT_TYPE) { if(parent_dom_node) { element = parent_dom_node; } else if(instance_children.length === 1 && template.children[0].type === HTML_TYPE) { return instance_children[0]; } else { element = doc.createElement("span"); } } else { element = doc.createElement(template.tag); } each(template.attributes, function(attr) { var name = attr.name, value = attr.value; if(name.match(name_regex)) { bindings.push((context[value] = getInputValueConstraint(element))); } else if((on_regex_match = name.match(on_regex))) { var event_name = on_regex_match[2]; aEL(element, event_name, bind(context[value], cjs.get(last(lineage).this_exp))); } else { var constraint = get_constraint(value, context, lineage); if(is_constraint(constraint)) { if(attr.name === "class") { var class_constraint = cjs(function() { var cval = constraint.get(); return cval.split(" "); }); bindings.push(constraint, class_constraint, class_binding(element, class_constraint)); } else { bindings.push(constraint, attr_binding(element, name, constraint)); } } else { element.setAttribute(attr.name, constraint); } } }); if(any_child_is_dynamic_html(template.children)) { // this is where it starts to suck...every child's innerHTML has to be taken and concatenated var concatenated_html = get_concatenated_inner_html_constraint(instance_children, context, lineage); binding = html_binding(element, concatenated_html); bindings.push(concatenated_html, binding); } else { var children_constraint = get_concatenated_children_constraint(instance_children, args); binding = children_binding(element, children_constraint); bindings.push(children_constraint, binding); } return { node: element, type: type, onAdd: function() { resume_each(bindings); onadd_each(instance_children); }, onRemove: function() { pause_each(bindings); onremove_each(instance_children); }, pause: function() { pause_each(instance_children.concat(bindings)); }, resume: function() { resume_each(instance_children.concat(bindings)); }, destroy: function() { destroy_each(instance_children.concat(bindings)); } }; } else if(type === UNARY_HB_TYPE) { var textNode, parsed_elem = template.obj, val_constraint = cjs(function() { return get_node_value(parsed_elem, context, lineage); }), node, txt_binding; if(!template.literal) { var curr_value = cjs.get(val_constraint); if(isPolyDOM(curr_value)) { node = getFirstDOMChild(curr_value); } else { node = doc.createTextNode(""+curr_value); txt_binding = text_binding(node, val_constraint); } } return { type: type, literal: template.literal, val: parsed_elem, node: node, destroy: function() { if(txt_binding) { txt_binding.destroy(true); } val_constraint.destroy(true); }, pause: function() { if(txt_binding) txt_binding.pause(); }, resume: function() { if(txt_binding) txt_binding.resume(); }, onRemove: function() { this.pause(); }, onAdd: function() { this.resume(); } }; } else if (type === HB_TYPE) { var tag = template.tag; if(tag === EACH_TAG) { var old_arr_val = [], arr_val, lastLineages = []; active_children = []; return { type: type, onRemove: function() { each(active_children, onremove_each); }, onAdd: function() { each(active_children, onadd_each); }, pause: function() { each(active_children, pause_each); }, resume: function() { each(active_children, resume_each); }, destroy: function() { each(active_children, destroy_each); active_children = []; }, getNodes: function() { arr_val = get_node_value(template.parsed_content, context, lineage); if(is_array(arr_val)) { // array constraint arr_val = arr_val.toArray(); } if(!isArray(arr_val)) { if(is_map(arr_val)) { // map constraint arr_val = arr_val.entries(); each(arr_val, function(x) { x.is_obj = IS_OBJ; }); } else { if(is_constraint(arr_val)) { arr_val = arr_val.get(); } // IS_OBJ provides a way to ensure the user didn't happen to pass in a similarly formatted array arr_val = map(arr_val, function(v, k) { return { key: k, value: v, is_obj: IS_OBJ }; }); } } else if(arr_val.length === 0 && template.else_child) { arr_val = [ELSE_COND]; } var diff = get_array_diff(old_arr_val, arr_val, map_aware_array_eq), rv = [], added_nodes = [], removed_nodes = []; old_arr_val = arr_val; each(diff.index_changed, function(ic_info) { var lastLineageItem = lastLineages[ic_info.from]; if(lastLineageItem && lastLineageItem.at && lastLineageItem.at.index) { lastLineageItem.at.index.set(ic_info.to); } }); each(diff.removed, function(removed_info) { var index = removed_info.from, lastLineageItem = lastLineages[index]; removed_nodes.push.apply(removed_nodes, active_children[index]); removeIndex(active_children, index); if(lastLineageItem && lastLineageItem.at) { each(lastLineageItem.at, function(v) { v.destroy(true); }); } }); each(diff.added, function(added_info) { var v = added_info.item, index = added_info.to, is_else = v === ELSE_COND, lastLineageItem = is_else ? false : ((v && v.is_obj === IS_OBJ) ? {this_exp: v.value , at: {key: cjs.constraint(v.key)}} : {this_exp: v, at: {index: cjs.constraint(index)}}), concated_lineage = is_else ? lineage : lineage.concat(lastLineageItem), children = is_else ? template.else_child.children : template.children, child_nodes = map(children, function(child) { return create_template_instance(child, context, concated_lineage); }); active_children.splice(index, 0, child_nodes); lastLineages.splice(index, 0, lastLineageItem); added_nodes.push.apply(added_nodes, child_nodes); }, this); each(diff.moved, function(moved_info) { var from_index = moved_info.from_index, to_index = moved_info.to_index, dom_elem = mdom[from_index], child_nodes = active_children[from_index], lastLineageItem = lastLineages[from_index]; removeIndex(active_children, from_index); active_children.splice(to_index, 0, child_nodes); removeIndex(lastLineages, from_index); lastLineages.splice(to_index, 0, lastLineageItem); }); onremove_each(removed_nodes); destroy_each(removed_nodes); onadd_each(added_nodes); var child_vals = map(active_children, function(child_nodes) { var instance_nodes = flatten(map(child_nodes, function(child_node) { return get_instance_nodes(child_node); }), true); return instance_nodes; }); return flatten(child_vals, true); } }; } else if(tag === IF_TAG || tag === UNLESS_TAG) { instance_children = []; active_children = []; var old_index = -1; return { type: type, onRemove: function() { onremove_each(active_children); }, onAdd: function() { onadd_each(active_children); }, pause: function() { pause_each(active_children); }, resume: function() { resume_each(active_children); }, destroy: function() { if(old_index >= 0) { active_children=[]; old_index=-1; } each(instance_children, destroy_each); }, getNodes: function() { var len = template.sub_conditions.length, cond = !!cjs.get(get_node_value(template.condition, context, lineage)), i, children = false, memo_index, rv; if(template.reverse) { cond = !cond; } if(cond) { i = 0; children = template.children; } else if(len > 0) { for(i = 0; i= 0 ? template_instances[nodeIndex] : false; }; extend(cjs, { /** * Create a new template. If `context` is specified, then this function returns a DOM node with the specified template. * Otherwise, it returns a function that can be called with `context` and `[parent]` to create a new template. * * ConstraintJS templates use a (Handlebars)[http://handlebarsjs.com/]. A template can be created with * `cjs.createTemplate`. The format is described below. * * ## Basics * ConstraintJS templates take standard HTML and add some features * * ### Constraints * Unary handlebars can contain expressions. * *

{{title}}

*

{{subtext.toUpperCase()+"!"}}

* * called with `{ title: cjs('hello'), subtext: 'world'}`: * *

hello

*

WORLD!

* * ### Literals * If the tags in a node should be treated as HTML, use triple braces: `{{{ literal_val }}}`. * These literals (triple braces) should be created immediately under a DOM node. * *

{{title}}

*

{{{subtext}}}

* * called with `{ title: cjs('hello'), subtext: 'steelhello *

steel city

* * * ## Comments * * {{! comments will be ignored in the output}} * * ## Constraint output * * To call `my_func` on event `(event-name)`, give any targets the attribute: * * data-cjs-on-(event-name)=my_func * * For example: * *
* * Will call `update_obj` (a property of the template's context when this div is clicked. * * To add the value of an input element to the template's context, use the property `data-cjs-out`: * * *

Hello, {{user_name}}

* * ## Block Helpers * * ### Loops * * To create an object for every item in an array or object, you can use the `{{#each}}` block helper. * `{{this}}` refers to the current item and `@key` and `@index` refer to the keys for arrays and objects * respectively. * * {{#each obj_name}} * {{@key}}: {{this}} * {{/each}} * * {{#each arr_name}} * {{@index}}: {{this}} * {{/each}} * * If the length of the array is zero (or the object has no keys) then an `{{#else}}` block can be used: * * {{#each arr_name}} * {{@index}}: {{this} * {{#else}} * No items! * {{/each}} * * ### Conditions * The `{{#if}}` block helper can vary the content of a template depending on some condition. * This block helper can have any number of sub-conditions with the related `{{#elif}}` and `{{#else}}` tags. * * {{#if cond1}} * Cond content * {{#elif other_cond}} * other_cond content * {{#else}} * else content * {{/if}} * * The opposite of an `{{#if}}` block is `{{#unless}}`: * {{#unless logged_in}} * Not logged in! * {{/unless} * * ### State * * The `{{#fsm}}` block helper can vary the content of a template depending on an FSM state * * {{#fsm my_fsm}} * {{#state1}} * State1 content * {{#state2}} * State2 content * {{#state3}} * State3 content * {{/fsm}} * * ### With helper * * The `{{#with}}` block helper changes the context in which constraints are evaluated. * * {{#with obj}} * Value: {{x}} * {{/with}} * * when called with `{ obj: {x: 1} }` results in `Value: 1` * * ## Partials * * Partials allow templates to be nested. * * var my_temp = cjs.createTemplate(...); * cjs.registerPartial('my_template', my_temp); * Then, in any other template, * * {{>my_template context}} * * Nests a copy of `my_template` in `context` * * @method cjs.createTemplate * @param {string|dom} template - the template as either a string or a `script` tag whose contents are the template * @param {object} [context] - Any number of target objects to listen to * @param {dom} [parent] - The parent DOM node for the template * @return {function|dom} - An event that can be attached to * * @see cjs.destroyTemplate * @see cjs.pauseTemplate * @see cjs.resumeTemplate * * @example * * * var template_elem = document.getElementById('my_template'); * var template = cjs.createTemplate(template_elem); * var element1 = template({x: 1}); * var element2 = template({x: 2}); * * @example * * var element = cjs.createTemplate("{{x}}", {x: 1}); */ createTemplate: function(template_str) { if(!isString(template_str)) { if(is_jquery_obj(template_str) || isNList(template_str)) { template_str = template_str.length > 0 ? trim(getTextContent(template_str[0])) : ""; } else if(isElement(template_str)) { template_str = trim(getTextContent(template_str)); } else { template_str = "" + template_str; } } var template = create_template(template_str); if(arguments.length >= 2) { // Create and use the template immediately return memoize_template.apply(template, rest(arguments)); } else { // create the template as a function that can be called with a context return bind(memoize_template, template); } }, /** * Register a *custom* partial that can be used in other templates * * Options are (only `createNode` is mandatory): * * `createNode(...)`: A function that returns a new dom node any time this partial is invoked (called with the arguments passed into the partial) * * `onAdd(dom_node)`: A function that is called when `dom_node` is added to the DOM tree * * `onRemove(dom_node)`: A function that is called when `dom_node` is removed from the DOM tree * * `pause(dom_node)`: A function that is called when the template has been paused (usually with `pauseTemplate`) * * `resume(dom_node)`: A function that is called when the template has been resumed (usually with `resumeTemplate`) * * `destroyNode(dom_node)`: A function that is called when the template has been destroyed (usually with `destroyTemplate`) * * @method cjs.registerCustomPartial * @param {string} name - The name that this partial can be referred to as * @param {Object} options - The set of options (described in the description) * @return {cjs} - `cjs` * @see cjs.registerPartial * @see cjs.unregisterPartial * @example Registering a custom partial named `my_custom_partial` * * cjs.registerCustomPartial('my_custom_partial', { * createNode: function(context) { * return document.createElement('span'); * }, * destroyNode: function(dom_node) { * // something like: completely_destroy(dom_node); * } * onAdd: function(dom_node) { * // something like: do_init(dom_node); * }, * onRemove: function(dom_node) { * // something like: cleanup(dom_node); * }, * pause: function(dom_node) { * // something like: pause_bindings(dom_node); * }, * resume: function(dom_node) { * // something like: resume_bindings(dom_node); * }, * }); * Then, in any other template, * * {{>my_template context}} * * Nests a copy of `my_template` in `context` */ registerCustomPartial: function(name, options) { custom_partials[name] = function() { var node = getFirstDOMChild(options.createNode.apply(this, arguments)); return { node: node, onAdd: function() { if(options.onAdd) { options.onAdd.apply(this, arguments); } }, onRemove: function() { if(options.onRemove) { options.onRemove.apply(this, arguments); } }, destroy: function() { if(options.destroyNode) { options.destroyNode.apply(this, arguments); } }, pause: function() { if(options.pause) { options.pause.apply(this, arguments); } }, resume: function() { if(options.resume) { options.resume.apply(this, arguments); } } }; }; return this; }, /** * Register a partial that can be used in other templates * * @method cjs.registerPartial * @param {string} name - The name that this partial can be referred to as * @param {Template} value - The template * @return {cjs} - `cjs` * @see cjs.unregisterPartial * @see cjs.registerCustomPartial * @example Registering a partial named `my_temp` * * var my_temp = cjs.createTemplate(...); * cjs.registerPartial('my_template', my_temp); * Then, in any other template, * * {{>my_template context}} * * Nests a copy of `my_template` in `context` */ registerPartial: function(name, value) { partials[name] = value; return this; }, /** * Unregister a partial for other templates * * @method cjs.unregisterPartial * @param {string} name - The name of the partial * @return {cjs} - `cjs` * @see cjs.registerPartial * @see cjs.registerCustomPartial */ unregisterPartial: function(name) { delete partials[name]; delete custom_partials[name]; return this; }, /** * Destroy a template instance * * @method cjs.destroyTemplate * @param {dom} node - The dom node created by `createTemplate` * @return {boolean} - Whether the template was successfully removed * @see cjs.createTemplate * @see cjs.pauseTemplate * @see cjs.resumeTemplate */ destroyTemplate: function(dom_node) { var index = get_template_instance_index(getFirstDOMChild(dom_node)), instance = index >= 0 ? template_instances[index] : false; if(instance) { delete template_instances[index]; instance.destroy(); } return this; }, /** * Pause dynamic updates to a template * * @method cjs.pauseTemplate * @param {dom} node - The dom node created by `createTemplate` * @return {boolean} - Whether the template was successfully paused * @see cjs.resumeTemplate * @see cjs.createTemplate * @see cjs.destroyTemplate */ pauseTemplate: function(dom_node) { var instance = get_template_instance(dom_node); if(instance) { instance.pause(); } return this; }, /** * Resume dynamic updates to a template * * @method cjs.resumeTemplate * @param {dom} node - The dom node created by `createTemplate` * @return {boolean} - Whether the template was successfully resumed * @see cjs.pauseTemplate * @see cjs.createTemplate * @see cjs.destroyTemplate */ resumeTemplate: function(dom_node) { var instance = get_template_instance(dom_node); if(instance) { instance.resume(); } return this; }, /** * Parses a string and returns a constraint whose value represents the result of `eval`ing * that string * * @method cjs.createParsedConstraint * @param {string} str - The string to parse * @param {object} context - The context in which to look for variables * @return {cjs.Cosntraint} - Whether the template was successfully resumed * @example Creating a parsed constraint `x` * * var a = cjs(1); * var x = cjs.createParsedConstraint("a+b", {a: a, b: cjs(2)}) * x.get(); // 3 * a.set(2); * x.get(); // 4 */ createParsedConstraint: function(str, context) { return cjs(function() { try { var node = jsep(cjs.get(str)); if(node.type === LITERAL) { return node.value; } else { return get_node_value(node, context, [context]); } } catch(e) { console.error(e); } }); } }); // Node Types // ---------- // This is the full set of types that any JSEP node can be. // Store them here to save space when minified var COMPOUND = 'Compound', IDENTIFIER = 'Identifier', MEMBER_EXP = 'MemberExpression', LITERAL = 'Literal', THIS_EXP = 'ThisExpression', CALL_EXP = 'CallExpression', UNARY_EXP = 'UnaryExpression', BINARY_EXP = 'BinaryExpression', LOGICAL_EXP = 'LogicalExpression', CONDITIONAL_EXP = 'ConditionalExpression', ARRAY_EXP = 'Array', PARENT_EXP = 'ParentExpression', CURR_LEVEL_EXP = 'CurrLevelExpression', PERIOD_CODE = 46, // '.' COMMA_CODE = 44, // ',' SQUOTE_CODE = 39, // single quote DQUOTE_CODE = 34, // double quotes OPAREN_CODE = 40, // ( CPAREN_CODE = 41, // ) OBRACK_CODE = 91, // [ CBRACK_CODE = 93, // ] QUMARK_CODE = 63, // ? SEMCOL_CODE = 59, // ; COLON_CODE = 58, // : throwError = function(message, index) { var error = new Error(message + ' at character ' + index); error.index = index; error.dedscription = message; throw error; }, jsep = (function() { // Operations // ---------- // Set `t` to `true` to save space (when minified, not gzipped) var t = true, // Use a quickly-accessible map to store all of the unary operators // Values are set to `true` (it really doesn't matter) unary_ops = {'-': t, '!': t, '~': t, '+': t}, // Also use a map for the binary operations but set their values to their // binary precedence for quick reference: // see [Order of operations](http://en.wikipedia.org/wiki/Order_of_operations#Programming_language) binary_ops = { '||': 1, '&&': 2, '|': 3, '^': 4, '&': 5, '==': 6, '!=': 6, '===': 6, '!==': 6, '<': 7, '>': 7, '<=': 7, '>=': 7, '<<':8, '>>': 8, '>>>': 8, '+': 9, '-': 9, '*': 10, '/': 10, '%': 10 }, // Get return the longest key length of any object getMaxKeyLen = function(obj) { var max_len = 0, len; for(var key in obj) { if((len = key.length) > max_len && obj.hasOwnProperty(key)) { max_len = len; } } return max_len; }, max_unop_len = getMaxKeyLen(unary_ops), max_binop_len = getMaxKeyLen(binary_ops), // Literals // ---------- // Store the values to return for the various literals we may encounter literals = { 'true': true, 'false': false, 'null': null }, // Except for `this`, which is special. This could be changed to something like `'self'` as well this_str = 'this', // Returns the precedence of a binary operator or `0` if it isn't a binary operator binaryPrecedence = function(op_val) { return binary_ops[op_val] || 0; }, // Utility function (gets called from multiple places) // Also note that `a && b` and `a || b` are *logical* expressions, not binary expressions createBinaryExpression = function (operator, left, right) { var type = (operator === '||' || operator === '&&') ? LOGICAL_EXP : BINARY_EXP; return { type: type, operator: operator, left: left, right: right }; }, // `ch` is a character code in the next three functions isDecimalDigit = function(ch) { return (ch >= 48 && ch <= 57); // 0...9 }, isIdentifierStart = function(ch) { return (ch === 36) || (ch === 95) || // `$` and `_` (ch >= 65 && ch <= 90) || // A...Z (ch === 64) || // @ (ch >= 97 && ch <= 122); // a...z }, isIdentifierPart = function(ch) { return (ch === 36) || (ch === 95) || // `$` and `_` (ch >= 65 && ch <= 90) || // A...Z (ch >= 97 && ch <= 122) || // a...z (ch >= 48 && ch <= 57); // 0...9 }, // Parsing // ------- // `expr` is a string with the passed in expression jsep = function(expr) { // `index` stores the character number we are currently at while `length` is a constant // All of the gobbles below will modify `index` as we move along var index = 0, charAtFunc = expr.charAt, charCodeAtFunc = expr.charCodeAt, exprI = function(i) { return charAtFunc.call(expr, i); }, exprICode = function(i) { return charCodeAtFunc.call(expr, i); }, length = expr.length, // Push `index` up to the next non-space character gobbleSpaces = function() { var ch = exprICode(index); // space or tab while(ch === 32 || ch === 9) { ch = exprICode(++index); } }, // The main parsing function. Much of this code is dedicated to ternary expressions gobbleExpression = function() { var test = gobbleBinaryExpression(), consequent, alternate; gobbleSpaces(); // Ternary expression: test ? consequent : alternate if(exprICode(index) === QUMARK_CODE) { index++; consequent = gobbleExpression(); if(!consequent) { throwError('Expected expression', index); } gobbleSpaces(); if(exprICode(index) === COLON_CODE) { index++; alternate = gobbleExpression(); if(!alternate) { throwError('Expected expression', index); } return { type: CONDITIONAL_EXP, test: test, consequent: consequent, alternate: alternate }; } else { throwError('Expected :', index); } } else { return test; } }, // Search for the operation portion of the string (e.g. `+`, `===`) // Start by taking the longest possible binary operations (3 characters: `===`, `!==`, `>>>`) // and move down from 3 to 2 to 1 character until a matching binary operation is found // then, return that binary operation gobbleBinaryOp = function() { gobbleSpaces(); var biop, to_check = expr.substr(index, max_binop_len), tc_len = to_check.length; while(tc_len > 0) { if(binary_ops.hasOwnProperty(to_check)) { index += tc_len; return to_check; } to_check = to_check.substr(0, --tc_len); } return false; }, // This function is responsible for gobbling an individual expression, // e.g. `1`, `1+2`, `a+(b*2)-Math.sqrt(2)` gobbleBinaryExpression = function() { var ch_i, node, biop, prec, stack, biop_info, left, right, i; // First, try to get the leftmost thing // Then, check to see if there's a binary operator operating on that leftmost thing left = gobbleToken(); biop = gobbleBinaryOp(); // If there wasn't a binary operator, just return the leftmost node if(!biop) { return left; } // Otherwise, we need to start a stack to properly place the binary operations in their // precedence structure biop_info = { value: biop, prec: binaryPrecedence(biop)}; right = gobbleToken(); if(!right) { throwError("Expected expression after " + biop, index); } stack = [left, biop_info, right]; // Properly deal with precedence using [recursive descent](http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm) while((biop = gobbleBinaryOp())) { prec = binaryPrecedence(biop); if(prec === 0) { break; } biop_info = { value: biop, prec: prec }; // Reduce: make a binary expression from the three topmost entries. while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) { right = stack.pop(); biop = stack.pop().value; left = stack.pop(); node = createBinaryExpression(biop, left, right); stack.push(node); } node = gobbleToken(); if(!node) { throwError("Expected expression after " + biop, index); } stack.push(biop_info, node); } i = stack.length - 1; node = stack[i]; while(i > 1) { node = createBinaryExpression(stack[i - 1].value, stack[i - 2], node); i -= 2; } return node; }, // An individual part of a binary expression: // e.g. `foo.bar(baz)`, `1`, `"abc"`, `(a % 2)` (because it's in parenthesis) gobbleToken = function() { var ch, curr_node, unop, to_check, tc_len; gobbleSpaces(); ch = exprICode(index); if(ch === PERIOD_CODE && expr.charCodeAt(index+1) === 47) { index += 2; return { type: CURR_LEVEL_EXP, argument: gobbleToken() }; } else if(ch === PERIOD_CODE && expr.charCodeAt(index+1) === PERIOD_CODE && expr.charCodeAt(index+2) === 47) { index += 3; return { type: PARENT_EXP, argument: gobbleToken() }; } if(isDecimalDigit(ch) || ch === PERIOD_CODE) { // Char code 46 is a dot `.` which can start off a numeric literal return gobbleNumericLiteral(); } else if(ch === SQUOTE_CODE || ch === DQUOTE_CODE) { // Single or double quotes return gobbleStringLiteral(); } else if(isIdentifierStart(ch) || ch === OPAREN_CODE) { // open parenthesis // `foo`, `bar.baz` return gobbleVariable(); } else { to_check = expr.substr(index, max_unop_len); tc_len = to_check.length; while(tc_len > 0) { if(unary_ops.hasOwnProperty(to_check)) { index += tc_len; return { type: UNARY_EXP, operator: to_check, argument: gobbleToken(), prefix: true }; } to_check = to_check.substr(0, --tc_len); } return false; } }, // Parse simple numeric literals: `12`, `3.4`, `.5`. Do this by using a string to // keep track of everything in the numeric literal and then calling `parseFloat` on that string gobbleNumericLiteral = function() { var number = '', ch; while(isDecimalDigit(exprICode(index))) { number += exprI(index++); } if(exprICode(index) === PERIOD_CODE) { // can start with a decimal marker number += exprI(index++); while(isDecimalDigit(exprICode(index))) { number += exprI(index++); } } ch = exprI(index); if(ch === 'e' || ch === 'E') { // exponent marker number += exprI(index++); ch = exprI(index); if(ch === '+' || ch === '-') { // exponent sign number += exprI(index++); } while(isDecimalDigit(exprICode(index))) { //exponent itself number += exprI(index++); } if(!isDecimalDigit(exprICode(index-1)) ) { throwError('Expected exponent (' + number + exprI(index) + ')', index); } } // Check to make sure this isn't a variable name that start with a number (123abc) if(isIdentifierStart(exprICode(index))) { throwError( 'Variable names cannot start with a number (' + number + exprI(index) + ')', index); } return { type: LITERAL, value: parseFloat(number), raw: number }; }, // Parses a string literal, staring with single or double quotes with basic support for escape codes // e.g. `"hello world"`, `'this is\nJSEP'` gobbleStringLiteral = function() { var str = '', quote = exprI(index++), closed = false, ch; while(index < length) { ch = exprI(index++); if(ch === quote) { closed = true; break; } else if(ch === '\\') { // Check for all of the common escape codes ch = exprI(index++); switch(ch) { case 'n': str += '\n'; break; case 'r': str += '\r'; break; case 't': str += '\t'; break; case 'b': str += '\b'; break; case 'f': str += '\f'; break; case 'v': str += '\x0B'; break; } } else { str += ch; } } if(!closed) { throwError('Unclosed quote after "'+str+'"', index); } return { type: LITERAL, value: str, raw: quote + str + quote }; }, // Gobbles only identifiers // e.g.: `foo`, `_value`, `$x1` // Also, this function checks if that identifier is a literal: // (e.g. `true`, `false`, `null`) or `this` gobbleIdentifier = function() { var ch = exprICode(index), start = index, identifier; if(isIdentifierStart(ch)) { index++; } else { throwError('Unexpected ' + exprI(index), index); } while(index < length) { ch = exprICode(index); if(isIdentifierPart(ch)) { index++; } else { break; } } identifier = expr.slice(start, index); if(literals.hasOwnProperty(identifier)) { return { type: LITERAL, value: literals[identifier], raw: identifier }; } else if(identifier === this_str) { return { type: THIS_EXP }; } else { return { type: IDENTIFIER, name: identifier }; } }, // Gobbles a list of arguments within the context of a function call // or array literal. This function also assumes that the opening character // `(` or `[` has already been gobbled, and gobbles expressions and commas // until the terminator character `)` or `]` is encountered. // e.g. `foo(bar, baz)`, `my_func()`, or `[bar, baz]` gobbleArguments = function(termination) { var ch_i, args = [], node; while(index < length) { gobbleSpaces(); ch_i = exprICode(index); if(ch_i === termination) { // done parsing index++; break; } else if (ch_i === COMMA_CODE) { // between expressions index++; } else { node = gobbleExpression(); if(!node || node.type === COMPOUND) { throwError('Expected comma', index); } args.push(node); } } return args; }, // Gobble a non-literal variable name. This variable name may include properties // e.g. `foo`, `bar.baz`, `foo['bar'].baz` // It also gobbles function calls: // e.g. `Math.acos(obj.angle)` gobbleVariable = function() { var ch_i, node; ch_i = exprICode(index); if(ch_i === OPAREN_CODE) { node = gobbleGroup(); } else { node = gobbleIdentifier(); } gobbleSpaces(); ch_i = exprICode(index); while(ch_i === PERIOD_CODE || ch_i === OBRACK_CODE || ch_i === OPAREN_CODE) { index++; if(ch_i === PERIOD_CODE) { gobbleSpaces(); node = { type: MEMBER_EXP, computed: false, object: node, property: gobbleIdentifier() }; } else if(ch_i === OBRACK_CODE) { node = { type: MEMBER_EXP, computed: true, object: node, property: gobbleExpression() }; gobbleSpaces(); ch_i = exprICode(index); if(ch_i !== CBRACK_CODE) { throwError('Unclosed [', index); } index++; } else if(ch_i === OPAREN_CODE) { // A function call is being made; gobble all the arguments node = { type: CALL_EXP, 'arguments': gobbleArguments(CPAREN_CODE), callee: node }; } gobbleSpaces(); ch_i = exprICode(index); } return node; }, // Responsible for parsing a group of things within parentheses `()` // This function assumes that it needs to gobble the opening parenthesis // and then tries to gobble everything within that parenthesis, assuming // that the next thing it should see is the close parenthesis. If not, // then the expression probably doesn't have a `)` gobbleGroup = function() { index++; var node = gobbleExpression(); gobbleSpaces(); if(exprICode(index) === CPAREN_CODE) { index++; return node; } else { throwError('Unclosed (', index); } }, // Responsible for parsing Array literals `[1, 2, 3]` // This function assumes that it needs to gobble the opening bracket // and then tries to gobble the expressions as arguments. gobbleArray = function() { index++; return { type: ARRAY_EXP, body: gobbleArguments(CBRACK_CODE) }; }, nodes = [], ch_i, node; while(index < length) { ch_i = exprICode(index); // Expressions can be separated by semicolons, commas, or just inferred without any // separators if(ch_i === SEMCOL_CODE || ch_i === COMMA_CODE) { index++; // ignore separators } else if (ch_i === OBRACK_CODE && (node = gobbleArray())) { nodes.push(node); } else { // Try to gobble each expression individually if((node = gobbleExpression())) { nodes.push(node); // If we weren't able to find a binary expression and are out of room, then // the expression passed in probably has too much } else if(index < length) { throwError('Unexpected "' + exprI(index) + '"', index); } } } // If there's only one expression just try returning the expression if(nodes.length === 1) { return nodes[0]; } else { return { type: COMPOUND, body: nodes }; } }; return jsep; }()); return cjs; }(this)); // Export for node if (typeof module !== 'undefined' && module.exports) { /** @exports cjs */ module.exports = cjs; }