// 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); } // ---------------------------------------------