diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e5f137 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/bower_components/ +/nbproject/ +/node_modules/ diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..7c49e89 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,45 @@ +module.exports = function(grunt) { + require('load-grunt-tasks')(grunt); + + directoryConfiguration = { + target: 'dist' + }; + + grunt.initConfig({ + dir: directoryConfiguration, + pkg: grunt.file.readJSON('package.json'), + banner: '/*\n' + + ' * Knockout Observable Dictionary\n' + + ' * (c) James Foster\n' + + ' * License: MIT (http://www.opensource.org/licenses/mit-license.php)\n' + + ' * <%= grunt.template.today("yyyy-mm-dd") %>\n' + + ' */', + clean: { + dist: '<%= dir.target %>', + }, + concat: { + options: { + separator: ';' + }, + js: { + src: ['<%= pkg.name %>.js'], + dest: '<%= dir.target %>/<%= pkg.name %>.js' + } + }, + uglify: { + dist: { + options: { + banner: '<%= banner %>', + sourceMap: true + }, + files: { + '<%= dir.target %>/<%= pkg.name %>.min.js': ['<%= concat.js.dest %>'] + } + } + } + }); + + grunt.registerTask('build', ['clean', 'concat:js']); + grunt.registerTask('minify', ['build', 'uglify']); + grunt.registerTask('default', ['minify']); +}; diff --git a/README.md b/README.md index cd0a718..c44a9e0 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ To loop over the items in the dictionary simply data bind to the items property. diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..6a11fe7 --- /dev/null +++ b/bower.json @@ -0,0 +1,24 @@ +{ + "name": "knockout-observable-dictionary", + "description": "Implementation of an obversable dictionary for Knockout.", + "main": "dist/ko.observableDictionary.js", + "authors": [ + { + "name": "James Foster", + "email": "james.foster@thap.co.uk" + } + ], + "ignore": [ + "*.html", + "bower.json", + "Gruntfile.js", + "package.json" + ], + "dependencies": { + "knockout": "~3" + }, + "repository": { + "type": "git", + "url": "git://github.com/jamesfoster/knockout.observableDictionary.git" + } +} diff --git a/dist/ko.observableDictionary.js b/dist/ko.observableDictionary.js new file mode 100644 index 0000000..3139d8c --- /dev/null +++ b/dist/ko.observableDictionary.js @@ -0,0 +1,224 @@ +// Knockout Observable Dictionary +// (c) James Foster +// License: MIT (http://www.opensource.org/licenses/mit-license.php) + +(function () { + function DictionaryItem(key, value, dictionary) { + var observableKey = new ko.observable(key); + + this.value = new ko.observable(value); + this.key = new ko.computed({ + read: observableKey, + write: function (newKey) { + var current = observableKey(); + + if (current == newKey) return; + + // no two items are allowed to share the same key. + dictionary.remove(newKey); + + observableKey(newKey); + } + }); + } + + ko.observableDictionary = function (dictionary, keySelector, valueSelector) { + var result = {}; + + result.items = new ko.observableArray(); + + result._wrappers = {}; + result._keySelector = keySelector || function (value, key) { return key; }; + result._valueSelector = valueSelector || function (value) { return value; }; + + if (typeof keySelector == 'string') result._keySelector = function (value) { return value[keySelector]; }; + if (typeof valueSelector == 'string') result._valueSelector = function (value) { return value[valueSelector]; }; + + ko.utils.extend(result, ko.observableDictionary['fn']); + + result.pushAll(dictionary); + + return result; + }; + + ko.observableDictionary['fn'] = { + remove: function (valueOrPredicate) { + var predicate = valueOrPredicate; + + if (valueOrPredicate instanceof DictionaryItem) { + predicate = function (item) { + return item.key() === valueOrPredicate.key(); + }; + } + else if (typeof valueOrPredicate != "function") { + predicate = function (item) { + return item.key() === valueOrPredicate; + }; + } + + ko.observableArray['fn'].remove.call(this.items, predicate); + }, + + push: function (key, value) { + var item = null; + + if (key instanceof DictionaryItem) { + // handle the case where only a DictionaryItem is passed in + item = key; + value = key.value(); + key = key.key(); + } + + if (value === undefined) { + value = this._valueSelector(key); + key = this._keySelector(value); + } + else { + value = this._valueSelector(value); + } + + var current = this.get(key, false); + if (current) { + // update existing value + current(value); + return current; + } + + if (!item) { + item = new DictionaryItem(key, value, this); + } + + ko.observableArray['fn'].push.call(this.items, item); + + return value; + }, + + pushAll: function (dictionary) { + var self = this; + var items = self.items(); + + if (dictionary instanceof Array) { + $.each(dictionary, function (index, item) { + var key = self._keySelector(item, index); + var value = self._valueSelector(item); + items.push(new DictionaryItem(key, value, self)); + }); + } + else { + for (var prop in dictionary) { + if (dictionary.hasOwnProperty(prop)) { + var item = dictionary[prop]; + var key = self._keySelector(item, prop); + var value = self._valueSelector(item); + items.push(new DictionaryItem(key, value, self)); + } + } + } + + self.items.valueHasMutated(); + }, + + sort: function (method) { + if (method === undefined) { + method = function (a, b) { + return defaultComparison(a.key(), b.key()); + }; + } + + return ko.observableArray['fn'].sort.call(this.items, method); + }, + + indexOf: function (key) { + if (key instanceof DictionaryItem) { + return ko.observableArray['fn'].indexOf.call(this.items, key); + } + + var underlyingArray = this.items(); + for (var index = 0; index < underlyingArray.length; index++) { + if (underlyingArray[index].key() == key) + return index; + } + return -1; + }, + + get: function (key, wrap) { + if (wrap == false) + return getValue(key, this.items()); + + var wrapper = this._wrappers[key]; + + if (wrapper == null) { + wrapper = this._wrappers[key] = new ko.computed({ + read: function () { + var value = getValue(key, this.items()); + return value ? value() : null; + }, + write: function (newValue) { + var value = getValue(key, this.items()); + + if (value) + value(newValue); + else + this.push(key, newValue); + } + }, this); + } + + return wrapper; + }, + + set: function (key, value) { + return this.push(key, value); + }, + + keys: function () { + return ko.utils.arrayMap(this.items(), function (item) { return item.key(); }); + }, + + values: function () { + return ko.utils.arrayMap(this.items(), function (item) { return item.value(); }); + }, + + removeAll: function () { + this.items.removeAll(); + }, + + toJSON: function () { + var result = {}; + var items = ko.utils.unwrapObservable(this.items); + + ko.utils.arrayForEach(items, function (item) { + var key = ko.utils.unwrapObservable(item.key); + var value = ko.utils.unwrapObservable(item.value); + + result[key] = value; + }); + + return result; + } + }; + + function getValue(key, items) { + var found = ko.utils.arrayFirst(items, function (item) { + return item.key() == key; + }); + return found ? found.value : null; + } +})(); + + +// Utility methods +// --------------------------------------------- +function isNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n); +} + +function defaultComparison(a, b) { + if (isNumeric(a) && isNumeric(b)) return a - b; + + a = a.toString(); + b = b.toString(); + + return a == b ? 0 : (a < b ? -1 : 1); +} +// --------------------------------------------- diff --git a/dist/ko.observableDictionary.min.js b/dist/ko.observableDictionary.min.js new file mode 100644 index 0000000..2ab92fa --- /dev/null +++ b/dist/ko.observableDictionary.min.js @@ -0,0 +1,8 @@ +/* + * Knockout Observable Dictionary + * (c) James Foster + * License: MIT (http://www.opensource.org/licenses/mit-license.php) + * 2014-06-20 + */ +function isNumeric(a){return!isNaN(parseFloat(a))&&isFinite(a)}function defaultComparison(a,b){return isNumeric(a)&&isNumeric(b)?a-b:(a=a.toString(),b=b.toString(),a==b?0:b>a?-1:1)}!function(){function a(a,b,c){var d=new ko.observable(a);this.value=new ko.observable(b),this.key=new ko.computed({read:d,write:function(a){var b=d();b!=a&&(c.remove(a),d(a))}})}function b(a,b){var c=ko.utils.arrayFirst(b,function(b){return b.key()==a});return c?c.value:null}ko.observableDictionary=function(a,b,c){var d={};return d.items=new ko.observableArray,d._wrappers={},d._keySelector=b||function(a,b){return b},d._valueSelector=c||function(a){return a},"string"==typeof b&&(d._keySelector=function(a){return a[b]}),"string"==typeof c&&(d._valueSelector=function(a){return a[c]}),ko.utils.extend(d,ko.observableDictionary.fn),d.pushAll(a),d},ko.observableDictionary.fn={remove:function(b){var c=b;b instanceof a?c=function(a){return a.key()===b.key()}:"function"!=typeof b&&(c=function(a){return a.key()===b}),ko.observableArray.fn.remove.call(this.items,c)},push:function(b,c){var d=null;b instanceof a&&(d=b,c=b.value(),b=b.key()),void 0===c?(c=this._valueSelector(b),b=this._keySelector(c)):c=this._valueSelector(c);var e=this.get(b,!1);return e?(e(c),e):(d||(d=new a(b,c,this)),ko.observableArray.fn.push.call(this.items,d),c)},pushAll:function(b){var c=this,d=c.items();if(b instanceof Array)$.each(b,function(b,e){var f=c._keySelector(e,b),g=c._valueSelector(e);d.push(new a(f,g,c))});else for(var e in b)if(b.hasOwnProperty(e)){var f=b[e],g=c._keySelector(f,e),h=c._valueSelector(f);d.push(new a(g,h,c))}c.items.valueHasMutated()},sort:function(a){return void 0===a&&(a=function(a,b){return defaultComparison(a.key(),b.key())}),ko.observableArray.fn.sort.call(this.items,a)},indexOf:function(b){if(b instanceof a)return ko.observableArray.fn.indexOf.call(this.items,b);for(var c=this.items(),d=0;d + + + TODO supply a title + + + + + +
+ +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..13a329a --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "knockout-observable-dictionary", + "devDependencies": { + "grunt": "~0", + "grunt-contrib-clean": "~0", + "grunt-contrib-concat": "~0", + "grunt-contrib-less": "~0", + "grunt-contrib-uglify": "*", + "load-grunt-tasks": "*" + } +}