can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
298 lines (286 loc) • 8.69 kB
JavaScript
steal('can/util/can.js', function (can) {
// Which batch of events this is for -- might not want to send multiple
// messages on the same batch. This is mostly for event delegation.
var batchNum = 1,
// how many times has start been called without a stop
transactions = 0,
dispatchingBatch = null,
collectingBatch = null,
batches = [],
dispatchingBatches = false;
can.batch = {
/**
* @function can.batch.start
* @parent can.batch
* @description Begin an event batch.
*
* @signature `can.batch.start([batchStopHandler])`
*
* @param {Function} [batchStopHandler] a callback that gets called after all batched events have been called
*
* @body
* `can.batch.start` causes can.Map to begin an event batch. Until `[can.batch.stop]` is called, any
* events that would result from calls to `[can.Map::attr attr]` are held back from firing. If you have
* lots of changes to make to can.Maps, batching them together can help performance &emdash; especially if
* those can.Maps are live-bound to the DOM.
*
* In this example, you can see how the _first_ and _change_ events are not fired (and their handlers
* are not called) until `can.batch.stop` is called.
*
* ```
* var person = new can.Map({
* first: 'Alexis',
* last: 'Abril'
* });
*
* person.bind('first', function() {
* console.log("First name changed."");
* }).bind('change', function() {
* console.log("Something changed.");
* });
*
* can.batch.start();
* person.attr('first', 'Alex');
* console.log('Still in the batch.');
* can.batch.stop();
*
* // the log has:
* // Still in the batch.
* // First name changed.
* // Something changed.
* ```
*
* You can also pass a callback to `can.batch.start` which will be called after all the events have
* been fired:
* ```
* can.batch.start(function() {
* console.log('The batch is over.');
* });
* person.attr('first', 'Izzy');
* console.log('Still in the batch.');
* can.batch.stop();
*
* // The console has:
* // Still in the batch.
* // First name changed.
* // Something changed.
* // The batch is over.
* ```
*
* ## Calling `can.batch.start` multiple times
*
* If you call `can.batch.start` more than once, `can.batch.stop` needs to be called
* the same number of times before any batched events will fire. For ways
* to circumvent this process, see [can.batch.stop].
*
* Here is an example that demonstrates how events are affected by calling
* `can.batch.start` multiple times.
*
* ```
* var addPeople = function(observable) {
* can.batch.start();
* observable.attr('a', 'Alice');
* observable.attr('b', 'Bob');
* observable.attr('e', 'Eve');
* can.batch.stop();
* };
*
* // In a completely different place:
* var list = new can.Map();
* list.bind('change', function() {
* console.log('The list changed.');
* });
*
* can.batch.start();
* addPeople(list);
* console.log('Still in the batch.');
*
* // Here, the console has:
* // Still in the batch.
*
* can.batch.stop();
*
* // Here, the console has:
* // Still in the batch.
* // The list changed.
* // The list changed.
* // The list changed.
* ```
*/
start: function (batchStopHandler) {
transactions++;
if(transactions === 1) {
var batch = {
events: [],
callbacks: [],
number: batchNum++
};
batches.push(batch);
if (batchStopHandler) {
batch.callbacks.push(batchStopHandler);
}
collectingBatch = batch;
}
},
/**
* @function can.batch.stop
* @parent can.batch
* @description End an event batch.
* @signature `can.batch.stop([force[, callStart]])`
* @param {bool} [force=false] whether to stop batching events immediately
* @param {bool} [callStart=false] whether to call `[can.batch.start can.batch.start]` after firing batched events
*
* @body
* `can.batch.stop` matches an earlier `[can.batch.start]` call. If `can.batch.stop` has been
* called as many times as `can.batch.start` (or if _force_ is true), all batched events will be
* fired and any callbacks passed to `can.batch.start` since the beginning of the batch will be
* called. If _force and _callStart_ are both true, a new batch will be started when all
* the events and callbacks have been fired.
*
* See `[can.batch.start]` for examples of `can.batch.start` and `can.batch.stop` in normal use.
*
* In this example, the batch is forceably ended in the `addPeople` function.
* ```
* var addPeople = function(observable) {
* can.batch.start();
* observable.attr('a', 'Alice');
* observable.attr('b', 'Bob');
* observable.attr('e', 'Eve');
* can.batch.stop(true);
* };
*
* // In a completely different place:
* var list = new can.Map();
* list.bind('change', function() {
* console.log('The list changed.');
* });
*
* can.batch.start();
* addPeople(list);
* console.log('Still in the batch.');
*
* // Here, the console has:
* // Still in the batch.
*
* can.batch.stop();
*
* // Here, the console has:
* // The list changed.
* // The list changed.
* // The list changed.
* // Still in the batch.
* ```
*/
stop: function (force, callStart) {
if (force) {
transactions = 0;
} else {
transactions--;
}
if (transactions === 0) {
collectingBatch = null;
var batch;
if(dispatchingBatches === false) {
dispatchingBatches = true;
var callbacks = [],
i;
while(batch = batches.shift()) {
var events = batch.events;
dispatchingBatch = batch;
can.batch.batchNum = batch.number;
var len;
if (callStart) {
can.batch.start();
}
for(i = 0, len = events.length; i < len; i++) {
can.dispatch.apply(events[i][0],events[i][1]);
}
can.batch._onDispatchedEvents(batch.number);
// NOTE: callbacks must be gathered up AFTER dispatching all events
// to ensure that callbacks registered by event handlers will be called.
callbacks.push.apply(callbacks, batch.callbacks );
dispatchingBatch = null;
can.batch.batchNum = undefined;
}
for(i = callbacks.length - 1; i >= 0 ; i--) {
callbacks[i]();
}
dispatchingBatches = false;
}
}
},
_onDispatchedEvents: function(){},
/**
* @function can.batch.trigger
* @parent can.batch
* @description Trigger an event to be added to the current batch.
* @signature `can.batch.trigger(item, event [, args])`
* @param {can.Map} item the target of the event
* @param {String|{type: String}} event the type of event, or an event object with a type given
* @param {Array} [args] the parameters to trigger the event with.
*
* @body
* If events are currently being batched, calling `can.batch.trigger` adds an event
* to the batch. If events are not currently being batched, the event is triggered
* immediately.
*/
trigger: function (item, event, args) {
// Don't send events if initalizing.
if (!item.__inSetup) {
event = typeof event === 'string' ? {
type: event
} : event;
// if there's a batch, add it to this batches events
if(collectingBatch) {
event.batchNum = collectingBatch.number;
collectingBatch.events.push([
item,
[event, args]
]);
}
// if this is trying to belong to another batch, let it fire
else if(event.batchNum) {
can.dispatch.call( item, event, args );
}
// if there are batches, but this doesn't belong to a batch
// add it to its own batch
else if(batches.length) {
can.batch.start();
event.batchNum = collectingBatch.number;
collectingBatch.events.push([
item,
[event, args]
]);
can.batch.stop();
}
// there are no batches, so just fire the event.
else {
can.dispatch.call( item, event, args );
}
}
},
// call handler after any events from currently settled stated have fired
// but before any future change events fire.
afterPreviousEvents: function(handler){
var batch = can.last(batches);
if(batch) {
var obj = {};
can.bind.call(obj,"ready", handler);
batch.events.push([
obj,
[{type: "ready"}, []]
]);
} else {
handler({});
}
},
after: function(handler){
var batch = collectingBatch || dispatchingBatch;
if(batch) {
batch.callbacks.push(handler);
} else {
handler({});
}
}
};
});