define(["lib/jquery", "lib/underscore", "./id_generator"], function($, _, IDGenerator) { "use strict"; var JQUERY_DATA_KEY = "lens:multimap:order"; // Map of id -> Element var elementIds = {}; // Gets the unique ID for an element by either reading its existing ID // or generating a new one. var getID = function(el) { if(!($(el).data("lens:multimap:id"))) { var id = IDGenerator.uuid(); $(el).data("lens:multimap:id", id); elementIds[id] = el; } return $(el).data("lens:multimap:id"); }; // If obj is an Element, returns a unique id for the element. Else, returns // obj.toString() var hash = function(obj) { if(obj.nodeType) { return getID(obj); } else { return obj.toString(); } }; /** * Creates a new MultiMap * * @class * Maps keys to an array of values. Supports using either Strings or DOM * Elements as keys. * * If the keys are DOM elements, they can be ordered by setting the * `lens:multimap:order` jQuery data. See also {@link MultiMap.updateOrder}, * which automatically creates a partial order for an array of elements. * * @name MultiMap * @private */ var MultiMap = function() { /** @alias MultiMap.prototype */ var self = {}; var map = {}; // given a map, returns an array of [key, value] pairs sorted by the // jQuery lens:multimap:order data on the key. Returns an underscore // wrapped array. var sortedMap = function(reverse) { return _.chain(map) .map(function(value, key) { // map key -> value pairs to [key, value] tuples, resolving // element ids to elements if(elementIds[key]) { key = elementIds[key]; } return [key, value]; }) .sortBy(function(pair) { if(pair[0].nodeType) { var order = $(pair[0]).data(JQUERY_DATA_KEY) || 0; if(reverse) { return -order; } else { return order; } } else { return undefined; } }); }; /** * Gets the array of values associated with a key. This DOES NOT * make a copy; modification to the returned array will modify this * MultiMap. */ self.get = function(key) { return map[hash(key)] || []; }; /** * Adds the given value to the array of values associated with a key */ self.put = function(key, value) { key = hash(key); if(key in map) { map[key].push(value); } else { map[key] = [value]; } return value; }; /** * Returns true iff the given key is in this MultiMap */ self.include = function(key) { key = hash(key); return (key in map); }; /** * Removes all instances of the given value from the array of values * associated with the given key. */ self.removeVal = function(key, value) { key = hash(key); var values = map[key]; if(!values) { return; } var newValues = _.without(values, value); if(newValues.length === 0) { // we're removing the last value delete map[key]; } else { map[key] = newValues; } }; /** * Removes the given key from this MultiMap. */ self.delete = function(key) { delete map[key]; }; /** * For each key in this MultiMap that has at least one value, invokes * fn(value, keys). If the jquery data for the key element includes * a lens:multimap:order property, the keys are sorted by that order. */ self.each = function(fn) { sortedMap().each(function(pair) { fn(pair[1], pair[0]); }); }; /** * The same as each, but traverses in reverse order. */ self.eachReverse = function(fn) { sortedMap(true).each(function(pair) { fn(pair[1], pair[0]); }); }; /** * The same as each, but stops execution if the callback function * returns false. Returns true if the callback returned true every * time. */ self.every = function(fn) { return sortedMap().every(function(pair) { return fn(pair[1], pair[0]); }).value(); }; /** * The same as every, but traverses in reverse order. */ self.everyReverse = function(fn) { return sortedMap(true).every(function(pair) { return fn(pair[1], pair[0]); }).value(); }; /** * Returns an array that contains the first value associated with * each key in this MultiMap. */ self.firsts = function() { return _.pluck(map, 0); }; /** * Returns a sorted array of [key, value] pairs. */ self.pairs = function() { return sortedMap().value(); }; /** * Returns the number of keys in this map. */ self.size = function() { var size = 0; for(var key in map) { if(map.hasOwnProperty(key)) { size++; } } return size; }; return self; }; _.extend(MultiMap, /** @lends MultiMap */ { /** * Updates the multimap order of the elements in the given list such * that no element has an order greater than an element that comes * after it. */ updateOrder: function(array) { if(array.length === 0) { return; } // update first element var order = $(array[0]).data(JQUERY_DATA_KEY); if(!_.isNumber(order)) { order = 0; $(array[0]).data(JQUERY_DATA_KEY, 0); } // if length=1, we're done if(array.length === 1) { return; } // update the rest of the array for(var i = 1; i < array.length; i++) { var parentOrder = $(array[i]).data(JQUERY_DATA_KEY); if(!_.isNumber(parentOrder)) { // parent has no multimap order; use child + 1 order++; $(array[i]).data(JQUERY_DATA_KEY, order); } else { // parent has a multimap order; use greater of child + 1 or its // existing order order = Math.max(order+1, parentOrder); $(array[i]).data("lens:multimap:order", order); } } } }); return MultiMap; });