UNPKG

todomvc

Version:

> Helping you select an MV\* framework

962 lines (898 loc) 27.7 kB
/*! * CanJS - 2.0.3 * http://canjs.us/ * Copyright (c) 2013 Bitovi * Tue, 26 Nov 2013 18:21:22 GMT * Licensed MIT * Includes: CanJS default build * Download from: http://canjs.us/ */ define(["can/util/library", "can/util/string", "can/util/object"], function (can) { // Get the URL from old Steal root, new Steal config or can.fixture.rootUrl var getUrl = function(url) { if(typeof steal !== 'undefined') { if(can.isFunction(steal.config)) { return steal.config().root.mapJoin(url).toString(); } return steal.root.join(url).toString(); } return (can.fixture.rootUrl || '') + url; } var updateSettings = function (settings, originalOptions) { if (!can.fixture.on) { return; } //simple wrapper for logging var _logger = function(type, arr){ if(console.log.apply){ Function.prototype.call.apply(console[type], [console].concat(arr)); // console[type].apply(console, arr) } else { console[type](arr) } }, log = function () { if(typeof steal !== 'undefined' && steal.dev) { steal.dev.log('fixture INFO: ' + Array.prototype.slice.call(arguments).join(' ')); } } // We always need the type which can also be called method, default to GET settings.type = settings.type || settings.method || 'GET'; // add the fixture option if programmed in var data = overwrite(settings); // if we don't have a fixture, do nothing if (!settings.fixture) { if (window.location.protocol === "file:") { log("ajax request to " + settings.url + ", no fixture found"); } return; } //if referencing something else, update the fixture option if (typeof settings.fixture === "string" && can.fixture[settings.fixture]) { settings.fixture = can.fixture[settings.fixture]; } // if a string, we just point to the right url if (typeof settings.fixture == "string") { var url = settings.fixture; if (/^\/\//.test(url)) { // this lets us use rootUrl w/o having steal... url = getUrl(settings.fixture.substr(2)); } if(data) { // Template static fixture URLs url = can.sub(url, data); } delete settings.fixture; settings.url = url; settings.data = null; settings.type = "GET"; if (!settings.error) { settings.error = function (xhr, error, message) { throw "fixtures.js Error " + error + " " + message; }; } } else { //it's a function ... add the fixture datatype so our fixture transport handles it // TODO: make everything go here for timing and other fun stuff // add to settings data from fixture ... settings.dataTypes && settings.dataTypes.splice(0, 0, "fixture"); if (data && originalOptions) { can.extend(originalOptions.data, data) } } }, // A helper function that takes what's called with response // and moves some common args around to make it easier to call extractResponse = function(status, statusText, responses, headers) { // if we get response(RESPONSES, HEADERS) if(typeof status != "number"){ headers = statusText; responses = status; statusText = "success" status = 200; } // if we get response(200, RESPONSES, HEADERS) if(typeof statusText != "string"){ headers = responses; responses = statusText; statusText = "success"; } if ( status >= 400 && status <= 599 ) { this.dataType = "text" } return [status, statusText, extractResponses(this, responses), headers]; }, // If we get data instead of responses, // make sure we provide a response type that matches the first datatype (typically json) extractResponses = function(settings, responses){ var next = settings.dataTypes ? settings.dataTypes[0] : (settings.dataType || 'json'); if (!responses || !responses[next]) { var tmp = {} tmp[next] = responses; responses = tmp; } return responses; }; //used to check urls // check if jQuery if (can.ajaxPrefilter && can.ajaxTransport) { // the pre-filter needs to re-route the url can.ajaxPrefilter(updateSettings); can.ajaxTransport("fixture", function (s, original) { // remove the fixture from the datatype s.dataTypes.shift(); //we'll return the result of the next data type var timeout, stopped = false; return { send: function (headers, callback) { // we'll immediately wait the delay time for all fixtures timeout = setTimeout(function () { // if the user wants to call success on their own, we allow it ... var success = function() { if(stopped === false) { callback.apply(null, extractResponse.apply(s, arguments) ); } }, // get the result form the fixture result = s.fixture(original, success, headers, s); if(result !== undefined) { // make sure the result has the right dataType callback(200, "success", extractResponses(s, result), {}); } }, can.fixture.delay); }, abort: function () { stopped = true; clearTimeout(timeout) } }; }); } else { var AJAX = can.ajax; can.ajax = function (settings) { updateSettings(settings, settings); if (settings.fixture) { var timeout, d = new can.Deferred(), stopped = false; //TODO this should work with response d.getResponseHeader = function () { } // call success and fail d.then(settings.success, settings.fail); // abort should stop the timeout and calling success d.abort = function () { clearTimeout(timeout); stopped = true; d.reject(d) } // set a timeout that simulates making a request .... timeout = setTimeout(function () { // if the user wants to call success on their own, we allow it ... var success = function() { var response = extractResponse.apply(settings, arguments), status = response[0]; if ( (status >= 200 && status < 300 || status === 304) && stopped === false) { d.resolve(response[2][settings.dataType]) } else { // TODO probably resolve better d.reject(d, 'error', response[1]); } }, // get the result form the fixture result = settings.fixture(settings, success, settings.headers, settings); if(result !== undefined) { d.resolve(result) } }, can.fixture.delay); return d; } else { return AJAX(settings); } } } var typeTest = /^(script|json|text|jsonp)$/, // a list of 'overwrite' settings object overwrites = [], // returns the index of an overwrite function find = function (settings, exact) { for (var i = 0; i < overwrites.length; i++) { if ($fixture._similar(settings, overwrites[i], exact)) { return i; } } return -1; }, // overwrites the settings fixture if an overwrite matches overwrite = function (settings) { var index = find(settings); if (index > -1) { settings.fixture = overwrites[index].fixture; return $fixture._getData(overwrites[index].url, settings.url) } }, // Makes an attempt to guess where the id is at in the url and returns it. getId = function (settings) { var id = settings.data.id; if (id === undefined && typeof settings.data === "number") { id = settings.data; } /* Check for id in params(if query string) If this is just a string representation of an id, parse if(id === undefined && typeof settings.data === "string") { id = settings.data; } //*/ if (id === undefined) { settings.url.replace(/\/(\d+)(\/|$|\.)/g, function (all, num) { id = num; }); } if (id === undefined) { id = settings.url.replace(/\/(\w+)(\/|$|\.)/g, function (all, num) { if (num != 'update') { id = num; } }) } if (id === undefined) { // if still not set, guess a random number id = Math.round(Math.random() * 1000) } return id; }; var $fixture = can.fixture = function (settings, fixture) { // if we provide a fixture ... if (fixture !== undefined) { if (typeof settings == 'string') { // handle url strings var matches = settings.match(/(GET|POST|PUT|DELETE) (.+)/i); if (!matches) { settings = { url : settings }; } else { settings = { url : matches[2], type : matches[1] }; } } //handle removing. An exact match if fixture was provided, otherwise, anything similar var index = find(settings, !!fixture); if (index > -1) { overwrites.splice(index, 1) } if (fixture == null) { return } settings.fixture = fixture; overwrites.push(settings) } else { can.each(settings, function(fixture, url){ $fixture(url, fixture); }) } }; var replacer = can.replacer; can.extend(can.fixture, { // given ajax settings, find an overwrite _similar : function (settings, overwrite, exact) { if (exact) { return can.Object.same(settings, overwrite, {fixture : null}) } else { return can.Object.subset(settings, overwrite, can.fixture._compare) } }, _compare : { url : function (a, b) { return !!$fixture._getData(b, a) }, fixture : null, type : "i" }, // gets data from a url like "/todo/{id}" given "todo/5" _getData : function (fixtureUrl, url) { var order = [], fixtureUrlAdjusted = fixtureUrl.replace('.', '\\.').replace('?', '\\?'), res = new RegExp(fixtureUrlAdjusted.replace(replacer, function (whole, part) { order.push(part) return "([^\/]+)" }) + "$").exec(url), data = {}; if (!res) { return null; } res.shift(); can.each(order, function (name) { data[name] = res.shift() }) return data; }, /** * @description Make a store of objects to use when making requests against fixtures. * @function can.fixture.store store * @parent can.fixture * * @signature `can.fixture.store(count, make[, filter])` * * @param {Number} count The number of items to create. * * @param {Function} make A function that will return the JavaScript object. The * make function is called back with the id and the current array of items. * * @param {Function} [filter] A function used to further filter results. Used for to simulate * server params like searchText or startDate. * The function should return true if the item passes the filter, * false otherwise. For example: * * * function(item, settings){ * if(settings.data.searchText){ * var regex = new RegExp("^"+settings.data.searchText) * return regex.test(item.name); * } * } * * @return {can.fixture.Store} A generator object providing fixture functions for *findAll*, *findOne*, *create*, * *update* and *destroy*. * * @body * `can.fixture.store(count, generator(index,items))` is used * to create a store of items that can simulate a full CRUD service. Furthermore, * the store can do filtering, grouping, sorting, and paging. * * ## Basic Example * * The following creates a store for 100 todos: * * var todoStore = can.fixture.store(100, function(i){ * return { * id: i, * name: "todo number "+i, * description: "a description of some todo", * ownerId: can.fixture.rand(10) * } * }) * * `todoStore`'s methods: * * - [can.fixture.types.Store.findAll findAll], * - [can.fixture.types.Store.findOne findOne], * - [can.fixture.types.Store.create create], * - [can.fixture.types.Store.update update], and * - [can.fixture.types.Store.destroy destroy] * * Can be used to simulate a REST service like: * * can.fixture({ * 'GET /todos': todoStore.findAll, * 'GET /todos/{id}': todoStore.findOne, * 'POST /todos': todoStore.create, * 'PUT /todos/{id}': todoStore.update, * 'DELETE /todos/{id}': todoStore.destroy * }); * * These fixtures, combined with a [can.Model] that connects to these services like: * * var Todo = can.Model.extend({ * findAll : 'GET /todos', * findOne : 'GET /todos/{id}', * create : 'POST /todos', * update : 'PUT /todos/{id}', * destroy : 'DELETE /todos/{id}' * }, {}); * * ... allows you to simulate requests for all of owner 5's todos like: * * Todo.findAll({ownerId: 5}, function(todos){ * * }) * * */ store: function (types, count, make, filter) { var items = [], // TODO: change this to a hash currentId = 0, findOne = function (id) { for (var i = 0; i < items.length; i++) { if (id == items[i].id) { return items[i]; } } }, methods = {}; if (typeof types === "string") { types = [types + "s", types ] } else if (!can.isArray(types)) { filter = make; make = count; count = types; } // make all items can.extend(methods, { /** * @description Simulate a findAll to a fixture. * @function can.fixture.types.Store.findAll * @parent can.fixture.types.Store * @signature `store.findAll(request)` * * `store.findAll(request)` simulates a request to * get a list items from the server. It supports the * following params: * * - order - `order=name ASC` * - group - `group=name` * - limit - `limit=20` * - offset - `offset=60` * - id filtering - `ownerId=5` * * * @param {{}} request The ajax request object. The available parameters are: * @option {String} order The order of the results. * `order: 'name ASC'` * @option {String} group How to group the results. * `group: 'name'` * @option {String} limit A limit on the number to retrieve. * `limit: 20` * @option {String} offset The offset of the results. * `offset: 60` * @option {String} id Filtering by ID. * `id: 5` * * @return {Object} a response object like: * * { * count: 1000, * limit: 20, * offset: 60, * data: [item1, item2, ...] * } * * where: * * - count - the number of items that match any filtering * before limit and offset is taken into account * - offset - the offset passed * - limit - the limit passed * - data - an array of JS objects with each item's properties * */ findAll: function (request) { request = request || {} //copy array of items var retArr = items.slice(0); request.data = request.data || {}; //sort using order //order looks like ["age ASC","gender DESC"] can.each((request.data.order || []).slice(0).reverse(), function (name) { var split = name.split(" "); retArr = retArr.sort(function (a, b) { if (split[1].toUpperCase() !== "ASC") { if (a[split[0]] < b[split[0]]) { return 1; } else if (a[split[0]] == b[split[0]]) { return 0 } else { return -1; } } else { if (a[split[0]] < b[split[0]]) { return -1; } else if (a[split[0]] == b[split[0]]) { return 0 } else { return 1; } } }); }); //group is just like a sort can.each((request.data.group || []).slice(0).reverse(), function (name) { var split = name.split(" "); retArr = retArr.sort(function (a, b) { return a[split[0]] > b[split[0]]; }); }); var offset = parseInt(request.data.offset, 10) || 0, limit = parseInt(request.data.limit, 10) || (items.length - offset), i = 0; //filter results if someone added an attr like parentId for (var param in request.data) { i = 0; if (request.data[param] !== undefined && // don't do this if the value of the param is null (ignore it) (param.indexOf("Id") != -1 || param.indexOf("_id") != -1)) { while (i < retArr.length) { if (request.data[param] != retArr[i][param]) { retArr.splice(i, 1); } else { i++; } } } } if (filter) { i = 0; while (i < retArr.length) { if (!filter(retArr[i], request)) { retArr.splice(i, 1); } else { i++; } } } //return data spliced with limit and offset return { "count" : retArr.length, "limit" : request.data.limit, "offset" : request.data.offset, "data" : retArr.slice(offset, offset + limit) }; }, /** * @description Simulate a findOne request on a fixture. * @function can.fixture.types.Store.findOne * @parent can.fixture.types.Store * @signature `store.findOne(request, callback)` * @param {Object} request Parameters for the request. * @param {Function} callback A function to call with the retrieved item. * * @body * `store.findOne(request, response(item))` simulates a request to * get a single item from the server by id. * * todosStore.findOne({ * url: "/todos/5" * }, function(todo){ * * }); * */ findOne : function (request, response) { var item = findOne(getId(request)); response(item ? item : undefined); }, /** * @description Simulate an update on a fixture. * @function can.fixture.types.Store.update * @parent can.fixture.types.Store * @signature `store.update(request, callback)` * @param {Object} request Parameters for the request. * @param {Function} callback A function to call with the updated item and headers. * * @body * `store.update(request, response(props,headers))` simulates * a request to update an items properties on a server. * * todosStore.update({ * url: "/todos/5" * }, function(props, headers){ * props.id //-> 5 * headers.location // "todos/5" * }); */ update: function (request,response) { var id = getId(request); // TODO: make it work with non-linear ids .. can.extend(findOne(id), request.data); response({ id : getId(request) }, { location : request.url || "/" + getId(request) }); }, /** * @description Simulate destroying a Model on a fixture. * @function can.fixture.types.Store.destroy * @parent can.fixture.types.Store * @signature `store.destroy(request, callback)` * @param {Object} request Parameters for the request. * @param {Function} callback A function to call after destruction. * * @body * `store.destroy(request, response())` simulates * a request to destroy an item from the server. * * @codestart * todosStore.destroy({ * url: "/todos/5" * }, function(){}); * @codeend */ destroy: function (request) { var id = getId(request); for (var i = 0; i < items.length; i++) { if (items[i].id == id) { items.splice(i, 1); break; } } // TODO: make it work with non-linear ids .. can.extend(findOne(id) || {}, request.data); return {}; }, /** * @description Simulate creating a Model with a fixture. * @function can.fixture.types.Store.create * @parent can.fixture.types.Store * @signature `store.create(request, callback)` * @param {Object} request Parameters for the request. * @param {Function} callback A function to call with the created item. * * @body * `store.destroy(request, callback)` simulates * a request to destroy an item from the server. * * @codestart * todosStore.create({ * url: "/todos" * }, function(){}); * @codeend */ create: function (settings, response) { var item = make(items.length, items); can.extend(item, settings.data); if (!item.id) { item.id = currentId++; } items.push(item); response({ id : item.id }, { location : settings.url + "/" + item.id }) } }); var reset = function(){ items = []; for (var i = 0; i < (count); i++) { //call back provided make var item = make(i, items); if (!item.id) { item.id = i; } currentId = Math.max(item.id+1, currentId+1) || items.length; items.push(item); } if(can.isArray(types)) { can.fixture["~" + types[0]] = items; can.fixture["-" + types[0]] = methods.findAll; can.fixture["-" + types[1]] = methods.findOne; can.fixture["-" + types[1]+"Update"] = methods.update; can.fixture["-" + types[1]+"Destroy"] = methods.destroy; can.fixture["-" + types[1]+"Create"] = methods.create; } } reset() // if we have types given add them to can.fixture return can.extend({ getId: getId, /** * @description Get an item from the store by ID. * @function can.fixture.types.Store.find * @parent can.fixture.types.Store * @signature `store.find(settings)` * @param {Object} settings An object containing an `id` key * corresponding to the item to find. * * @body * `store.find(settings)` * `store.destroy(request, callback)` simulates a request to * get a single item from the server. * * @codestart * todosStore.find({ * url: "/todos/5" * }, function(){}); * @codeend */ find: function(settings){ return findOne( getId(settings) ); }, /** * @description Reset the fixture store. * @function can.fixture.types.Store.reset * @parent can.fixture.types.Store * @signature `store.reset()` * * @body * `store.reset()` resets the store to contain its * original data. This is useful for making tests that * operate independently. * * ## Basic Example * * After creating a `taskStore` and hooking it up to a * `task` model in the "Basic Example" in [can.fixture.store store's docs], * a test might create several tasks like: * * new Task({name: "Take out trash", ownerId: 5}).save(); * * But, another test might need to operate on the original set of * tasks created by `can.fixture.store`. Reset the task store with: * * taskStore.reset() * */ reset: reset }, methods); }, /** * @description Create a random number or selection. * @function can.fixture.rand rand * @parent can.fixture * @signature `can.fixture.rand([min,] max)` * @param {Number} [min=0] The lower bound on integers to select. * @param {Number} max The upper bound on integers to select. * @return {Number} A random integer in the range [__min__, __max__). * * @signature `can.fixture.rand(choices, min[ ,max])` * @param {Array} choices An array of things to choose from. * @param {Number} min The minimum number of times to choose from __choices__. * @param {Number} [max=min] The maximum number of times to choose from __choices__. * @return {Array} An array of between __min__ and __max__ random choices from __choices__. * * @body * `can.fixture.rand` creates random integers or random arrays of * other arrays. * * ## Examples * * var rand = can.fixture.rand; * * // get a random integer between 0 and 10 (inclusive) * rand(11); * * // get a random number between -5 and 5 (inclusive) * rand(-5, 6); * * // pick a random item from an array * rand(["j","m","v","c"],1)[0] * * // pick a random number of items from an array * rand(["j","m","v","c"]) * * // pick 2 items from an array * rand(["j","m","v","c"],2) * * // pick between 2 and 3 items at random * rand(["j","m","v","c"],2,3) */ rand : function (arr, min, max) { if (typeof arr == 'number') { if (typeof min == 'number') { return arr + Math.floor(Math.random() * (min - arr)); } else { return Math.floor(Math.random() * arr); } } var rand = arguments.callee; // get a random set if (min === undefined) { return rand(arr, rand(arr.length + 1)) } // get a random selection of arr var res = []; arr = arr.slice(0); // set max if (!max) { max = min; } //random max max = min + Math.round(rand(max - min)) for (var i = 0; i < max; i++) { res.push(arr.splice(rand(arr.length), 1)[0]) } return res; }, /** * @hide * * Use can.fixture.xhr to create an object that looks like an xhr object. * * ## Example * * The following example shows how the -restCreate fixture uses xhr to return * a simulated xhr object: * @codestart * "-restCreate" : function( settings, cbType ) { * switch(cbType){ * case "success": * return [ * {id: parseInt(Math.random()*1000)}, * "success", * can.fixture.xhr()]; * case "complete": * return [ * can.fixture.xhr({ * getResponseHeader: function() { * return settings.url+"/"+parseInt(Math.random()*1000); * } * }), * "success"]; * } * } * @codeend * @param {Object} [xhr] properties that you want to overwrite * @return {Object} an object that looks like a successful XHR object. */ xhr : function (xhr) { return can.extend({}, { abort : can.noop, getAllResponseHeaders : function () { return ""; }, getResponseHeader : function () { return ""; }, open : can.noop, overrideMimeType : can.noop, readyState : 4, responseText : "", responseXML : null, send : can.noop, setRequestHeader : can.noop, status : 200, statusText : "OK" }, xhr); }, /** * @property {Boolean} can.fixture.on on * @parent can.fixture * * `can.fixture.on` lets you programatically turn off fixtures. This is mostly used for testing. * * can.fixture.on = false * Task.findAll({}, function(){ * can.fixture.on = true; * }) */ on : true }); /** * @property {Number} can.fixture.delay delay * @parent can.fixture * * `can.fixture.delay` indicates the delay in milliseconds between an ajax request is made and * the success and complete handlers are called. This only sets * functional synchronous fixtures that return a result. By default, the delay is 200ms. * * @codestart * steal('can/util/fixtures').then(function(){ * can.fixture.delay = 1000; * }) * @codeend */ can.fixture.delay = 200; /** * @property {String} can.fixture.rootUrl rootUrl * @parent can.fixture * * `can.fixture.rootUrl` contains the root URL for fixtures to use. * If you are using StealJS it will use the Steal root * URL by default. */ can.fixture.rootUrl = getUrl(''); can.fixture["-handleFunction"] = function (settings) { if (typeof settings.fixture === "string" && can.fixture[settings.fixture]) { settings.fixture = can.fixture[settings.fixture]; } if (typeof settings.fixture == "function") { setTimeout(function () { if (settings.success) { settings.success.apply(null, settings.fixture(settings, "success")); } if (settings.complete) { settings.complete.apply(null, settings.fixture(settings, "complete")); } }, can.fixture.delay); return true; } return false; }; //Expose this for fixture debugging can.fixture.overwrites = overwrites; can.fixture.make = can.fixture.store; return can.fixture; });