UNPKG

can-map

Version:

Observable Objects

237 lines (205 loc) 7.92 kB
"use strict"; // # can/map/map_helpers // Helpers that enable bubbling of an event on a child object to a // parent event on a parent object. Bubbling works by listening on the child object // and forwarding events to the parent object. // // Bubbling is complicated because bubbling setup can happen before or after // items are added to the parent object. // // This means that: // - When bubbling is first initialied, by binding to an event that bubbles, // all child objects need to be setup to bubble. This is managed by [bubble.bind](#bubble-bind). // - When bubbling is stopped, by removing all listeners to events that bubble, // all child objects need to have bubbling torn down. This is managed by [bubble.unbind](#bubble-unbind). // - While bubbling is running, as child items are added, // the child elements need to be setup to bubble. This is managed by [bubble.add](#bubble-add) and [bubble.addMany](#bubble-addmany). // - While bubbling is running, as child items are removed, // the child elements need to stop bubbling. This is managed by // [bubble.remove](#bubble-remove) and [bubble.removeMany](#bubble-removeMany). // - While bubbling is running, as child item replaces another child, the old child needs bubbling removed // and the new child needs bubbling setup. This is managed by [bubble.set](bubble-set). // // [bubble.events](bubble-events) controls which events setup bubbling. var canEvent = require('can-event-queue/map/map'); var canReflect = require('can-reflect'); // Helper function to check the Map type var isMap = function(map) { return ( map && !canReflect.isFunctionLike(map) && canReflect.isMapLike(map) && !Array.isArray(map) && canReflect.isObservableLike(map) ); }; var bubble = { // ## bubble.bind // Called when an event is bound to an object. This // should setup bubbling if this is the first time // an event that bubbles is bound. bind: function(parent, eventName) { if (!parent.__inSetup ) { var bubbleEvents = bubble.events(parent, eventName), len = bubbleEvents.length, bubbleEvent; if(!parent._bubbleBindings) { parent._bubbleBindings = {}; } for (var i = 0; i < len; i++) { bubbleEvent = bubbleEvents[i]; // If there isn't a bubbling setup for this binding, // bubble all the children; otherwise, increment the // number of bubble bindings. if (!parent._bubbleBindings[bubbleEvent]) { parent._bubbleBindings[bubbleEvent] = 1; bubble.childrenOf(parent, bubbleEvent); } else { parent._bubbleBindings[bubbleEvent]++; } } } }, // ## bubble.unbind // Called when an event is unbound from an object. This should // teardown bubbling if there are no more bubbling event handlers. unbind: function(parent, eventName) { var bubbleEvents = bubble.events(parent, eventName), len = bubbleEvents.length, bubbleEvent; for (var i = 0; i < len; i++) { bubbleEvent = bubbleEvents[i]; if (parent._bubbleBindings ) { parent._bubbleBindings[bubbleEvent]--; } if (parent._bubbleBindings && !parent._bubbleBindings[bubbleEvent] ) { delete parent._bubbleBindings[bubbleEvent]; bubble.teardownChildrenFrom(parent, bubbleEvent); if(canReflect.size(parent._bubbleBindings) === 0) { delete parent._bubbleBindings; } } } }, // ## bubble.add // Called when a new `child` value has been added to `parent`. // If the `parent` is bubbling and the child is observable, // setup bubbling on the child to the parent. This calls // `teardownFromParent` to ensure we aren't bubbling the same // child more than once. add: function(parent, child, prop){ if(canReflect.isObservableLike(child) && canReflect.isMapLike(child) && parent._bubbleBindings) { for(var eventName in parent._bubbleBindings) { if( parent._bubbleBindings[eventName] ) { bubble.teardownFromParent(parent, child, eventName); bubble.toParent(child, parent, prop, eventName); } } } }, // ## bubble.addMany // Called when many `children` are added to `parent`. addMany: function(parent, children){ for (var i = 0, len = children.length; i < len; i++) { bubble.add(parent, children[i], i); } }, // ## bubble.remove // Called when a `child` has been removed from `parent`. // Removes all bubbling events from `child` to `parent`. remove: function(parent, child){ if(canReflect.isObservableLike(child) && canReflect.isMapLike(child) && parent._bubbleBindings) { for(var eventName in parent._bubbleBindings) { if( parent._bubbleBindings[eventName] ) { bubble.teardownFromParent(parent, child, eventName); } } } }, // ## bubble.removeMany // Called when many `children` are removed from `parent`. removeMany: function(parent, children){ for(var i = 0, len = children.length; i < len; i++) { bubble.remove(parent, children[i]); } }, // ## bubble.set // Called when a new child `value` replaces `current` value. set: function(parent, prop, value, current){ if(canReflect.isObservableLike(value) && canReflect.isMapLike(value)) { bubble.add(parent, value, prop); } // bubble.add will remove, so only remove if we are replacing another object if(canReflect.isObservableLike(current) && canReflect.isMapLike(current)) { bubble.remove(parent, current); } return value; }, // ## bubble.events // For an event binding on an object, returns the events that should be bubbled. // For example, `"change" -> ["change"]`. events: function(map, boundEventName) { if (isMap(map)) { return map.constructor._bubbleRule(boundEventName, map); } }, // ## bubble.toParent // Forwards an event on `child` to `parent`. `child` is // the `prop` property of `parent`. toParent: function(child, parent, prop, eventName) { canEvent.listenTo.call(parent, child, eventName, function ( /* ev, attr */ ) { var args = canReflect.toArray(arguments), ev = args.shift(); // Updates the nested property name that will be dispatched. // If the parent is a list, the index of the child needs to // be calculated every time. args[0] = ((canReflect.isObservableLike(parent) && canReflect.isListLike(parent)) ? parent.indexOf(child) : prop ) + (args[0] ? "."+args[0] : ""); // Track all objects that we have bubbled this event to. // If we have already bubbled to this object, do not dispatch another // event on it. This prevents cycles. ev.triggeredNS = ev.triggeredNS || {}; if (ev.triggeredNS[parent._cid]) { return; } ev.triggeredNS[parent._cid] = true; // Send bubbled event to parent. canEvent.dispatch.call(parent, ev, args); // Trigger named event. if(eventName === "change") { canEvent.dispatch.call(parent, args[0], [args[2], args[3]]); } }); }, // ## bubble.childrenOf // Bubbles all the observable children of `parent`. childrenOf: function (parent, eventName) { parent._each(function (child, prop) { if (isMap(child)) { bubble.toParent(child, parent, prop, eventName); } }); }, // ## bubble.teardownFromParent // Undo the bubbling from `child` to `parent`. teardownFromParent: function (parent, child, eventName ) { if(child && child.unbind ) { canEvent.stopListening.call(parent, child, eventName); } }, // ## bubble.teardownChildrenFrom // Undo the bubbling of every child of `parent` teardownChildrenFrom: function(parent, eventName){ parent._each(function (child) { bubble.teardownFromParent(parent, child, eventName); }); }, // ## bubble.isBubbling // Returns true or false if `parent` is bubbling `eventName`. isBubbling: function(parent, eventName){ return parent._bubbleBindings && parent._bubbleBindings[eventName]; } }; module.exports = bubble;