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