From ffba1c0c8eeede128b48bb83ce61018e291f1ef2 Mon Sep 17 00:00:00 2001 From: Kevin Simper Date: Sun, 12 Jan 2014 00:25:02 +0100 Subject: [PATCH 1/3] More documentation for storage function --- src/storage.js | 621 ++++--------------------------------------------- 1 file changed, 41 insertions(+), 580 deletions(-) diff --git a/src/storage.js b/src/storage.js index 5cdcebed..c6c5791f 100644 --- a/src/storage.js +++ b/src/storage.js @@ -8,608 +8,69 @@ var Crafty = require('./core.js'), /**@ * #Storage * @category Utilities - * Utility to allow data to be saved to a permanent storage solution: IndexedDB, WebSql, localstorage or cookies + * Very simple way to get and set values, which will persist when the browser is closed also. */ /**@ - * #.open + * #.storage * @comp Storage - * @sign .open(String gameName) - * @param gameName - a machine readable string to uniquely identify your game + * @sign .storage(String key) + * @param key - a key you would like to get from the storage. It will return null if the key does not exists. + * @sign .storage(String key, String value) + * @param key - the key you would like to save the data under. + * @param value - the value you would like to save. + * @sign .storage(String key, [Object value, Array value, Boolean value]) + * @param key - the key you would like to save the data under. + * @param value - the value you would like to save, can be an Object or an Array. * - * Opens a connection to the database. If the best they have is localstorage or lower, it does nothing + * Storage function is very simple and can be used to either get or set values. + * You can store both booleans, strings, objects and arrays. * * @example - * Open a database + * Get an already stored value * ~~~ - * Crafty.storage.open('MyGame'); + * var playername = Crafty.storage('playername'); * ~~~ - */ -/**@ - * #.save - * @comp Storage - * @sign .save(String key, String type, Mixed data) - * @param key - A unique key for identifying this piece of data - * @param type - 'save' or 'cache' - * @param data - Some kind of data. - * - * Saves a piece of data to the database. Can be anything, although entities are preferred. - * For all storage methods but IndexedDB, the data will be serialized as a string - * During serialization, an entity's SaveData event will be triggered. - * Components should implement a SaveData handler and attach the necessary information to the passed object * * @example - * Saves an entity to the database + * Save a value * ~~~ - * var ent = Crafty.e("2D, DOM") - * .attr({x: 20, y: 20, w: 100, h:100}); - * Crafty.storage.open('MyGame'); - * Crafty.storage.save('MyEntity', 'save', ent); + * Crafty.storage('playername', 'Hero'); * ~~~ - */ -/**@ - * #.load - * @comp Storage - * @sign .load(String key, String type) - * @param key - A unique key to search for - * @param type - 'save' or 'cache' - * @param callback - Do things with the data you get back - * - * Loads a piece of data from the database. - * Entities will be reconstructed from the serialized string - - * @example - * Loads an entity from the database - * ~~~ - * Crafty.storage.open('MyGame'); - * Crafty.storage.load('MyEntity', 'save', function (data) { // do things }); - * ~~~ - */ -/**@ - * #.getAllKeys - * @comp Storage - * @sign .getAllKeys(String type) - * @param type - 'save' or 'cache' - * Gets all the keys for a given type - - * @example - * Gets all the save games saved - * ~~~ - * Crafty.storage.open('MyGame'); - * var saves = Crafty.storage.getAllKeys('save'); - * ~~~ - */ -/**@ - * #.external - * @comp Storage - * @sign .external(String url) - * @param url - URL to an external to save games too - * - * Enables and sets the url for saving games to an external server - * - * @example - * Save an entity to an external server - * ~~~ - * Crafty.storage.external('http://somewhere.com/server.php'); - * Crafty.storage.open('MyGame'); - * var ent = Crafty.e('2D, DOM') - * .attr({x: 20, y: 20, w: 100, h:100}); - * Crafty.storage.save('save01', 'save', ent); - * ~~~ - */ -/**@ - * #SaveData event - * @comp Storage - * @param data - An object containing all of the data to be serialized - * @param prepare - The function to prepare an entity for serialization - * - * Any data a component wants to save when it's serialized should be added to this object. - * Straight attribute should be set in data.attr. - * Anything that requires a special handler should be set in a unique property. * * @example - * Saves the innerHTML of an entity + * Test to see if a value is already there. * ~~~ - * Crafty.e("2D DOM").bind("SaveData", function (data, prepare) { - * data.attr.x = this.x; - * data.attr.y = this.y; - * data.dom = this.element.innerHTML; - * }); + * var heroname = Crafty.storage('name'); + * if(!heroname){ + * // Maybe ask the player what their name is here + * heroname = 'Guest'; + * } + * // Do something with heroname * ~~~ */ -/**@ - * #LoadData event - * @comp Storage - * @param data - An object containing all the data that been saved - * @param process - The function to turn a string into an entity - * - * Handlers for processing any data that needs more than straight assignment - * - * Note that data stored in the .attr object is automatically added to the entity. - * It does not need to be handled here - * - * @example - * ~~~ - * Sets the innerHTML from a saved entity - * Crafty.e("2D DOM").bind("LoadData", function (data, process) { - * this.element.innerHTML = data.dom; - * }); - * ~~~ - */ -Crafty.storage = (function () { - var db = null, - url, gameName, timestamps = {}, - transactionType = { - READ: "readonly", - READ_WRITE: "readwrite" - }; - - /* - * Processes a retrieved object. - * Creates an entity if it is one - */ - - function process(obj) { - if (obj.c) { - var d = Crafty.e(obj.c) - .attr(obj.attr) - .trigger('LoadData', obj, process); - return d; - } else if (typeof obj == 'object') { - for (var prop in obj) { - obj[prop] = process(obj[prop]); - } - } - return obj; - } - - function unserialize(str) { - if (typeof str != 'string') return null; - var data = (JSON ? JSON.parse(str) : eval('(' + str + ')')); - return process(data); - } - /* recursive function - * searches for entities in an object and processes them for serialization - */ +Crafty.storage = function(key, value){ + var storage = window.localStorage, + _value = value; - function prep(obj) { - if (obj.__c) { - // object is entity - var data = { - c: [], - attr: {} - }; - obj.trigger("SaveData", data, prep); - for (var i in obj.__c) { - data.c.push(i); - } - data.c = data.c.join(', '); - obj = data; - } else if (typeof obj == 'object') { - // recurse and look for entities - for (var prop in obj) { - obj[prop] = prep(obj[prop]); - } - } - return obj; + if(arguments.length === 1) { + try { + return JSON.parse(storage.getItem(key)); } - - function serialize(e) { - if (JSON) { - var data = prep(e); - return JSON.stringify(data); - } else { - alert("Crafty does not support saving on your browser. Please upgrade to a newer browser."); - return false; - } + catch (e) { + return storage.getItem(key); } - - // for saving a game to a central server - - function external(setUrl) { - url = setUrl; + } else { + if(typeof value === "object") { + _value = JSON.stringify(value); } - function openExternal() { - if (1 && typeof url == "undefined") return; - // get the timestamps for external saves and compare them to local - // if the external is newer, load it + storage.setItem(key, _value); + + } - var xml = new XMLHttpRequest(); - xhr.open("POST", url); - xhr.onreadystatechange = function (evt) { - if (xhr.readyState == 4) { - if (xhr.status == 200) { - var data = eval("(" + xhr.responseText + ")"); - for (var i in data) { - if (Crafty.storage.check(data[i].key, data[i].timestamp)) { - loadExternal(data[i].key); - } - } - } - } - }; - xhr.send("mode=timestamps&game=" + gameName); - } +}; - function saveExternal(key, data, ts) { - if (1 && typeof url == "undefined") return; - var xhr = new XMLHttpRequest(); - xhr.open("POST", url); - xhr.send("mode=save&key=" + key + "&data=" + encodeURIComponent(data) + "&ts=" + ts + "&game=" + gameName); - } - - function loadExternal(key) { - if (1 && typeof url == "undefined") return; - var xhr = new XMLHttpRequest(); - xhr.open("POST", url); - xhr.onreadystatechange = function (evt) { - if (xhr.readyState == 4) { - if (xhr.status == 200) { - var data = eval("(" + xhr.responseText + ")"); - Crafty.storage.save(key, 'save', data); - } - } - }; - xhr.send("mode=load&key=" + key + "&game=" + gameName); - } - - /** - * get timestamp - */ - - function ts() { - var d = new Date(); - return d.getTime(); - } - - // everyone names their object different. Fix that nonsense. - if (typeof indexedDB != 'object') { - window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; - window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction; - window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange; - - /* Numeric constants for transaction type are deprecated - * Ensure that the script will work consistenly for recent and legacy browser versions - */ - if (typeof IDBTransaction == 'object') { - transactionType.READ = IDBTransaction.READ || IDBTransaction.readonly || transactionType.READ || 'read'; - transactionType.READ_WRITE = IDBTransaction.READ_WRITE || IDBTransaction.readwrite || transactionType.READ_WRITE || 'readwrite'; - } - } - - if (typeof indexedDB == 'object') { - - return { - open: function (gameName_n) { - gameName = gameName_n; - var stores = []; - - if (arguments.length == 1) { - stores.push('save'); - stores.push('cache'); - } else { - stores = arguments; - stores.shift(); - stores.push('save'); - stores.push('cache'); - } - if (db === null) { - var request = indexedDB.open(gameName); - request.onsuccess = function (e) { - db = e.target.result; - getTimestamps(); - openExternal(); - }; - request.onupgradeneeded = function (e) { - createStores(); - }; - } else { - createStores(); - getTimestamps(); - openExternal(); - } - - // get all the timestamps for existing keys - - function getTimestamps() { - try { - var trans = db.transaction(['save'], "read"), - store = trans.objectStore('save'), - request = store.getAll(); - request.onsuccess = function (e) { - var i = 0, - a = event.target.result, - l = a.length; - for (; i < l; i++) { - timestamps[a[i].key] = a[i].timestamp; - } - }; - } catch (e) {} - } - - function createStores() { - var request = db.setVersion("1.0"); - request.onsuccess = function (e) { - for (var i = 0; i < stores.length; i++) { - var st = stores[i]; - if (db.objectStoreNames.contains(st)) continue; - var store = db.createObjectStore(st, { - keyPath: "key" - }); - } - }; - } - }, - - save: function (key, type, data, callback) { - if (db === null) { - setTimeout(function () { - Crafty.storage.save(key, type, data); - }, 1); - return; - } - - var str = serialize(data), - t = ts(); - if (type == 'save') saveExternal(key, str, t); - try { - var request = db.transaction([type], transactionType.READ_WRITE).objectStore(type).add({ - "data": str, - "timestamp": t, - "key": key - }); - if (typeof callback == 'function') { - request.onsuccess = callback; - } - } catch (e) { - console.error(e); - } - }, - - load: function (key, type, callback) { - if (db === null) { - setTimeout(function () { - Crafty.storage.load(key, type, callback); - }, 1); - return; - } - try { - var request = db.transaction([type], transactionType.READ).objectStore(type).get(key); - request.onsuccess = function (e) { - callback(unserialize(e.target.result.data)); - }; - } catch (e) { - console.error(e); - } - }, - - getAllKeys: function (type, callback) { - if (db === null) { - setTimeout(function () { - Crafty.storage.getAllkeys(type, callback); - }, 1); - } - try { - var request = db.transaction([type], transactionType.READ).objectStore(type).openCursor(), - res = []; - request.onsuccess = function (e) { - var cursor = e.target.result; - if (cursor) { - res.push(cursor.key); - // 'continue' is a reserved word, so .continue() causes IE8 to completely bark with "SCRIPT1010: Expected identifier". - cursor['continue'](); - } else { - callback(res); - } - }; - } catch (e) { - console.error(e); - } - }, - - check: function (key, timestamp) { - return (timestamps[key] > timestamp); - }, - - external: external - }; - } else if (typeof openDatabase == 'function') { - return { - open: function (gameName_n) { - gameName = gameName_n; - if (arguments.length == 1) { - db = { - save: openDatabase(gameName_n + '_save', '1.0', 'Saves games for ' + gameName_n, 5 * 1024 * 1024), - cache: openDatabase(gameName_n + '_cache', '1.0', 'Cache for ' + gameName_n, 5 * 1024 * 1024) - }; - } else { - // allows for any other types that can be thought of - var args = arguments, - i = 0; - args.shift(); - for (; i < args.length; i++) { - if (typeof db[args[i]] == 'undefined') - db[args[i]] = openDatabase(gameName + '_' + args[i], '1.0', type, 5 * 1024 * 1024); - } - } - - db.save.transaction(function (tx) { - tx.executeSql('SELECT key, timestamp FROM data', [], function (tx, res) { - var i = 0, - a = res.rows, - l = a.length; - for (; i < l; i++) { - timestamps[a.item(i).key] = a.item(i).timestamp; - } - }); - }); - }, - - save: function (key, type, data) { - if (typeof db[type] == 'undefined' && gameName !== '') { - this.open(gameName, type); - } - - var str = serialize(data), - t = ts(); - if (type == 'save') saveExternal(key, str, t); - db[type].transaction(function (tx) { - tx.executeSql('CREATE TABLE IF NOT EXISTS data (key unique, text, timestamp)'); - tx.executeSql('SELECT * FROM data WHERE key = ?', [key], function (tx, results) { - if (results.rows.length) { - tx.executeSql('UPDATE data SET text = ?, timestamp = ? WHERE key = ?', [str, t, key]); - } else { - tx.executeSql('INSERT INTO data VALUES (?, ?, ?)', [key, str, t]); - } - }); - }); - }, - - load: function (key, type, callback) { - if (typeof db[type] === 'undefined') { - setTimeout(function () { - Crafty.storage.load(key, type, callback); - }, 1); - return; - } - db[type].transaction(function (tx) { - tx.executeSql('SELECT text FROM data WHERE key = ?', [key], function (tx, results) { - if (results.rows.length) { - res = unserialize(results.rows.item(0).text); - callback(res); - } - }); - }); - }, - - getAllKeys: function (type, callback) { - if (typeof db[type] === 'undefined') { - setTimeout(function () { - Crafty.storage.getAllKeys(type, callback); - }, 1); - return; - } - db[type].transaction(function (tx) { - tx.executeSql('SELECT key FROM data', [], function (tx, results) { - callback(results.rows); - }); - }); - }, - - check: function (key, timestamp) { - return (timestamps[key] > timestamp); - }, - - external: external - }; - } else if (typeof window.localStorage == 'object') { - return { - open: function (gameName_n) { - gameName = gameName_n; - }, - - save: function (key, type, data) { - var k = gameName + '.' + type + '.' + key, - str = serialize(data), - t = ts(); - if (type == 'save') saveExternal(key, str, t); - window.localStorage[k] = str; - if (type == 'save') - window.localStorage[k + '.ts'] = t; - }, - - load: function (key, type, callback) { - var k = gameName + '.' + type + '.' + key, - str = window.localStorage[k]; - - callback(unserialize(str)); - }, - - getAllKeys: function (type, callback) { - var res = {}, output = [], - header = gameName + '.' + type; - for (var i in window.localStorage) { - if (i.indexOf(header) != -1) { - var key = i.replace(header, '').replace('.ts', ''); - res[key] = true; - } - } - for (i in res) { - output.push(i); - } - callback(output); - }, - - check: function (key, timestamp) { - var ts = window.localStorage[gameName + '.save.' + key + '.ts']; - - return (parseInt(timestamp, 10) > parseInt(ts, 10)); - }, - - external: external - }; - } else { - // default fallback to cookies - return { - open: function (gameName_n) { - gameName = gameName_n; - }, - - save: function (key, type, data) { - // cookies are very limited in space. we can only keep saves there - if (type != 'save') return; - var str = serialize(data), - t = ts(); - if (type == 'save') saveExternal(key, str, t); - document.cookie = gameName + '_' + key + '=' + str + '; ' + gameName + '_' + key + '_ts=' + t + '; expires=Thur, 31 Dec 2099 23:59:59 UTC; path=/'; - }, - - load: function (key, type, callback) { - if (type != 'save') return; - var reg = new RegExp(gameName + '_' + key + '=[^;]*'), - result = reg.exec(document.cookie), - data = unserialize(result[0].replace(gameName + '_' + key + '=', '')); - - callback(data); - }, - - getAllKeys: function (type, callback) { - if (type != 'save') return; - var reg = new RegExp(gameName + '_[^_=]', 'g'), - matches = reg.exec(document.cookie), - i = 0, - l = matches.length, - res = {}, output = []; - for (; i < l; i++) { - var key = matches[i].replace(gameName + '_', ''); - res[key] = true; - } - for (i in res) { - output.push(i); - } - callback(output); - }, - - check: function (key, timestamp) { - var header = gameName + '_' + key + '_ts', - reg = new RegExp(header + '=[^;]'), - result = reg.exec(document.cookie), - ts = result[0].replace(header + '=', ''); - - return (parseInt(timestamp, 10) > parseInt(ts, 10)); - }, - - external: external - }; - } - /* template - return { - open: function (gameName) { - }, - save: function (key, type, data) { - }, - load: function (key, type, callback) { - }, - }*/ -})(); \ No newline at end of file +Crafty.storage.remove = function(key){ + window.localStorage.removeItem(key); +}; \ No newline at end of file From 3558a92fe23df2f801e1c8e357b01972a3dce09d Mon Sep 17 00:00:00 2001 From: Kevin Simper Date: Sun, 12 Jan 2014 00:25:39 +0100 Subject: [PATCH 2/3] Added the storage to the test again New tests for storage.js --- tests/index.html | 1 + tests/storage.js | 88 +++++++++++++----------------------------------- 2 files changed, 25 insertions(+), 64 deletions(-) diff --git a/tests/index.html b/tests/index.html index dc435be1..4a1dfdf7 100755 --- a/tests/index.html +++ b/tests/index.html @@ -54,6 +54,7 @@

+ diff --git a/tests/storage.js b/tests/storage.js index a67c692f..da43b83e 100644 --- a/tests/storage.js +++ b/tests/storage.js @@ -1,73 +1,33 @@ -module("Storage"); -test("saveAndLoadObject", function() { - Crafty.storage.open("MyGame"); - Crafty.storage.save("LeaderBoard", "save", { - name: "Matthew", - score: 150 - }); - stop(); - Crafty.storage.load("LeaderBoard", "save", function(lb) { - equal(lb.name, "Matthew"); - start(); - }); +module("Storage", { + setup: function() { + reset(); + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } }); -test("saveAndLoadArray", function() { - Crafty.storage.open("MyGame1"); - Crafty.storage.save("LeaderBoard", "save", [{ - name: "Matthew", - score: 150 - }, { - name: "Louis", - score: 17 - }]); - stop(); - Crafty.storage.load("LeaderBoard", "save", function(lb) { - equal(lb[1].name, "Louis"); - equal(lb.length, 2); - start(); - }); -}); +test('get a value', function(){ + Crafty.storage('name', 'test'); + var name = Crafty.storage('name'); -test("saveAndLoadEntity", function() { - Crafty.storage.open("MyGame2"); - Crafty.storage.save("Hero", "save", Crafty.e("2D, DOM").attr({ - x: 20, - y: 137 - })); - stop(); - Crafty.storage.load("Hero", "save", function(hero) { - console.log(hero); - ok(hero.__c["2D"]); - ok(hero.__c.DOM); - equal(hero.x, 0, "Entity state is not saved"); - start(); - }); -}); + equal(name, 'test', 'the values should be equal'); -test("individualNamespaces", function() { - Crafty.storage.open("MyGame3"); - Crafty.storage.save("LeaderBoard", "save", { - name: "Matthew", - score: 150 - }); + Crafty.storage.remove('name'); +}); +test('get null when a value does not exist', function(){ + var name = Crafty.storage('notexisting'); + equal(name, null, 'should be null'); +}); - Crafty.storage.open("MyGame4"); - Crafty.storage.save("LeaderBoard", "save", { - name: "Louis", - score: 150 - }); +test('remove an value', function(){ + var person = Crafty.storage('person', 'test'); + equal(Crafty.storage('person'), 'test', 'person should be defined'); - stop(); - Crafty.storage.load("LeaderBoard", "save", function(lb) { - equal(lb.name, "Louis"); - start(); - }); + Crafty.storage.remove('person'); - Crafty.storage.open("MyGame3"); - Crafty.storage.load("LeaderBoard", "save", function(lb) { - equal(lb.name, "Matthew"); - start(); - }); + var savedperson = Crafty.storage('person'); + equal(savedperson, null, 'should be null because we just removed the value'); }); \ No newline at end of file From f61e5f55eba050a67de346a9b5d685f2a806e787 Mon Sep 17 00:00:00 2001 From: Kevin Simper Date: Wed, 15 Jan 2014 00:37:33 +0100 Subject: [PATCH 3/3] Updated the documentation for the storage function explaining when to use it If the localStorage object is not supported return false. Added documentation for remove function --- src/storage.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/storage.js b/src/storage.js index c6c5791f..66962158 100644 --- a/src/storage.js +++ b/src/storage.js @@ -1,10 +1,6 @@ var Crafty = require('./core.js'), document = window.document; -// Directive for jshint to ignore evals -// eval is used to support IE8, so this can be removed if we decide to drop that -/* jshint evil:true */ - /**@ * #Storage * @category Utilities @@ -25,6 +21,8 @@ var Crafty = require('./core.js'), * Storage function is very simple and can be used to either get or set values. * You can store both booleans, strings, objects and arrays. * + * Please note: You should not store data, while the game is playing, as it can cause the game to slow down. You should load data when you start the game, or when the user for an example click a "Save gameprocess" button. + * * @example * Get an already stored value * ~~~ @@ -53,6 +51,10 @@ Crafty.storage = function(key, value){ var storage = window.localStorage, _value = value; + if(!storage){ + return false; + } + if(arguments.length === 1) { try { return JSON.parse(storage.getItem(key)); @@ -70,7 +72,23 @@ Crafty.storage = function(key, value){ } }; - +/**@ + * #.storage.remove + * @comp Storage + * @sign .storage.remove(String key) + * @param key - a key where you will like to delete the value of. + * + * Generally you do not need to remove values from localStorage, but if you do + * store large amount of text, or want to unset something you can do that with + * this function. + * + * @example + * Get an already stored value + * ~~~ + * Crafty.storage.remove('playername'); + * ~~~ + * + */ Crafty.storage.remove = function(key){ window.localStorage.removeItem(key); }; \ No newline at end of file