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": "*"
+ }
+}