/** * Durandal 2.1.0 Copyright (c) 2012 Blue Spire Consulting, Inc. All Rights Reserved. * Available via the MIT license. * see: http://durandaljs.com or https://github.com/BlueSpire/Durandal for details. */ /** * The activator module encapsulates all logic related to screen/component activation. * An activator is essentially an asynchronous state machine that understands a particular state transition protocol. * The protocol ensures that the following series of events always occur: `canDeactivate` (previous state), `canActivate` (new state), `deactivate` (previous state), `activate` (new state). * Each of the _can_ callbacks may return a boolean, affirmative value or promise for one of those. If either of the _can_ functions yields a false result, then activation halts. * @module activator * @requires system * @requires knockout */ define(['durandal/system', 'knockout'], function (system, ko) { var activator; var defaultOptions = { canDeactivate:true }; function ensureSettings(settings) { if (settings == undefined) { settings = {}; } if (!system.isBoolean(settings.closeOnDeactivate)) { settings.closeOnDeactivate = activator.defaults.closeOnDeactivate; } if (!settings.beforeActivate) { settings.beforeActivate = activator.defaults.beforeActivate; } if (!settings.afterDeactivate) { settings.afterDeactivate = activator.defaults.afterDeactivate; } if(!settings.affirmations){ settings.affirmations = activator.defaults.affirmations; } if (!settings.interpretResponse) { settings.interpretResponse = activator.defaults.interpretResponse; } if (!settings.areSameItem) { settings.areSameItem = activator.defaults.areSameItem; } if (!settings.findChildActivator) { settings.findChildActivator = activator.defaults.findChildActivator; } return settings; } function invoke(target, method, data) { if (system.isArray(data)) { return target[method].apply(target, data); } return target[method](data); } function deactivate(item, close, settings, dfd, setter) { if (item && item.deactivate) { system.log('Deactivating', item); var result; try { result = item.deactivate(close); } catch(error) { system.log('ERROR: ' + error.message, error); dfd.resolve(false); return; } if (result && result.then) { result.then(function() { settings.afterDeactivate(item, close, setter); dfd.resolve(true); }, function(reason) { system.log(reason); dfd.resolve(false); }); } else { settings.afterDeactivate(item, close, setter); dfd.resolve(true); } } else { if (item) { settings.afterDeactivate(item, close, setter); } dfd.resolve(true); } } function activate(newItem, activeItem, callback, activationData) { var result; if(newItem && newItem.activate) { system.log('Activating', newItem); try { result = invoke(newItem, 'activate', activationData); } catch(error) { system.log('ERROR: ' + error.message, error); callback(false); return; } } if(result && result.then) { result.then(function() { activeItem(newItem); callback(true); }, function(reason) { system.log('ERROR: ' + reason.message, reason); callback(false); }); } else { activeItem(newItem); callback(true); } } function canDeactivateItem(item, close, settings, options) { options = system.extend({}, defaultOptions, options); settings.lifecycleData = null; return system.defer(function (dfd) { function continueCanDeactivate() { if (item && item.canDeactivate && options.canDeactivate) { var resultOrPromise; try { resultOrPromise = item.canDeactivate(close); } catch (error) { system.log('ERROR: ' + error.message, error); dfd.resolve(false); return; } if (resultOrPromise.then) { resultOrPromise.then(function (result) { settings.lifecycleData = result; dfd.resolve(settings.interpretResponse(result)); }, function (reason) { system.log('ERROR: ' + reason.message, reason); dfd.resolve(false); }); } else { settings.lifecycleData = resultOrPromise; dfd.resolve(settings.interpretResponse(resultOrPromise)); } } else { dfd.resolve(true); } } var childActivator = settings.findChildActivator(item); if (childActivator) { childActivator.canDeactivate().then(function(result) { if (result) { continueCanDeactivate(); } else { dfd.resolve(false); } }); } else { continueCanDeactivate(); } }).promise(); }; function canActivateItem(newItem, activeItem, settings, activeData, newActivationData) { settings.lifecycleData = null; return system.defer(function (dfd) { if (settings.areSameItem(activeItem(), newItem, activeData, newActivationData)) { dfd.resolve(true); return; } if (newItem && newItem.canActivate) { var resultOrPromise; try { resultOrPromise = invoke(newItem, 'canActivate', newActivationData); } catch (error) { system.log('ERROR: ' + error.message, error); dfd.resolve(false); return; } if (resultOrPromise.then) { resultOrPromise.then(function(result) { settings.lifecycleData = result; dfd.resolve(settings.interpretResponse(result)); }, function(reason) { system.log('ERROR: ' + reason.message, reason); dfd.resolve(false); }); } else { settings.lifecycleData = resultOrPromise; dfd.resolve(settings.interpretResponse(resultOrPromise)); } } else { dfd.resolve(true); } }).promise(); }; /** * An activator is a read/write computed observable that enforces the activation lifecycle whenever changing values. * @class Activator */ function createActivator(initialActiveItem, settings) { var activeItem = ko.observable(null); var activeData; settings = ensureSettings(settings); var computed = ko.computed({ read: function () { return activeItem(); }, write: function (newValue) { computed.viaSetter = true; computed.activateItem(newValue); } }); computed.__activator__ = true; /** * The settings for this activator. * @property {ActivatorSettings} settings */ computed.settings = settings; settings.activator = computed; /** * An observable which indicates whether or not the activator is currently in the process of activating an instance. * @method isActivating * @return {boolean} */ computed.isActivating = ko.observable(false); computed.forceActiveItem = function (item) { activeItem(item); }; /** * Determines whether or not the specified item can be deactivated. * @method canDeactivateItem * @param {object} item The item to check. * @param {boolean} close Whether or not to check if close is possible. * @param {object} options Options for controlling the activation process. * @return {promise} */ computed.canDeactivateItem = function (item, close, options) { return canDeactivateItem(item, close, settings, options); }; /** * Deactivates the specified item. * @method deactivateItem * @param {object} item The item to deactivate. * @param {boolean} close Whether or not to close the item. * @return {promise} */ computed.deactivateItem = function (item, close) { return system.defer(function(dfd) { computed.canDeactivateItem(item, close).then(function(canDeactivate) { if (canDeactivate) { deactivate(item, close, settings, dfd, activeItem); } else { computed.notifySubscribers(); dfd.resolve(false); } }); }).promise(); }; /** * Determines whether or not the specified item can be activated. * @method canActivateItem * @param {object} item The item to check. * @param {object} activationData Data associated with the activation. * @return {promise} */ computed.canActivateItem = function (newItem, activationData) { return canActivateItem(newItem, activeItem, settings, activeData, activationData); }; /** * Activates the specified item. * @method activateItem * @param {object} newItem The item to activate. * @param {object} newActivationData Data associated with the activation. * @param {object} options Options for controlling the activation process. * @return {promise} */ computed.activateItem = function (newItem, newActivationData, options) { var viaSetter = computed.viaSetter; computed.viaSetter = false; return system.defer(function (dfd) { if (computed.isActivating()) { dfd.resolve(false); return; } computed.isActivating(true); var currentItem = activeItem(); if (settings.areSameItem(currentItem, newItem, activeData, newActivationData)) { computed.isActivating(false); dfd.resolve(true); return; } computed.canDeactivateItem(currentItem, settings.closeOnDeactivate, options).then(function (canDeactivate) { if (canDeactivate) { computed.canActivateItem(newItem, newActivationData).then(function (canActivate) { if (canActivate) { system.defer(function (dfd2) { deactivate(currentItem, settings.closeOnDeactivate, settings, dfd2); }).promise().then(function () { newItem = settings.beforeActivate(newItem, newActivationData); activate(newItem, activeItem, function (result) { activeData = newActivationData; computed.isActivating(false); dfd.resolve(result); }, newActivationData); }); } else { if (viaSetter) { computed.notifySubscribers(); } computed.isActivating(false); dfd.resolve(false); } }); } else { if (viaSetter) { computed.notifySubscribers(); } computed.isActivating(false); dfd.resolve(false); } }); }).promise(); }; /** * Determines whether or not the activator, in its current state, can be activated. * @method canActivate * @return {promise} */ computed.canActivate = function () { var toCheck; if (initialActiveItem) { toCheck = initialActiveItem; initialActiveItem = false; } else { toCheck = computed(); } return computed.canActivateItem(toCheck); }; /** * Activates the activator, in its current state. * @method activate * @return {promise} */ computed.activate = function () { var toActivate; if (initialActiveItem) { toActivate = initialActiveItem; initialActiveItem = false; } else { toActivate = computed(); } return computed.activateItem(toActivate); }; /** * Determines whether or not the activator, in its current state, can be deactivated. * @method canDeactivate * @return {promise} */ computed.canDeactivate = function (close) { return computed.canDeactivateItem(computed(), close); }; /** * Deactivates the activator, in its current state. * @method deactivate * @return {promise} */ computed.deactivate = function (close) { return computed.deactivateItem(computed(), close); }; computed.includeIn = function (includeIn) { includeIn.canActivate = function () { return computed.canActivate(); }; includeIn.activate = function () { return computed.activate(); }; includeIn.canDeactivate = function (close) { return computed.canDeactivate(close); }; includeIn.deactivate = function (close) { return computed.deactivate(close); }; }; if (settings.includeIn) { computed.includeIn(settings.includeIn); } else if (initialActiveItem) { computed.activate(); } computed.forItems = function (items) { settings.closeOnDeactivate = false; settings.determineNextItemToActivate = function (list, lastIndex) { var toRemoveAt = lastIndex - 1; if (toRemoveAt == -1 && list.length > 1) { return list[1]; } if (toRemoveAt > -1 && toRemoveAt < list.length - 1) { return list[toRemoveAt]; } return null; }; settings.beforeActivate = function (newItem) { var currentItem = computed(); if (!newItem) { newItem = settings.determineNextItemToActivate(items, currentItem ? items.indexOf(currentItem) : 0); } else { var index = items.indexOf(newItem); if (index == -1) { items.push(newItem); } else { newItem = items()[index]; } } return newItem; }; settings.afterDeactivate = function (oldItem, close) { if (close) { items.remove(oldItem); } }; var originalCanDeactivate = computed.canDeactivate; computed.canDeactivate = function (close) { if (close) { return system.defer(function (dfd) { var list = items(); var results = []; function finish() { for (var j = 0; j < results.length; j++) { if (!results[j]) { dfd.resolve(false); return; } } dfd.resolve(true); } for (var i = 0; i < list.length; i++) { computed.canDeactivateItem(list[i], close).then(function (result) { results.push(result); if (results.length == list.length) { finish(); } }); } }).promise(); } else { return originalCanDeactivate(); } }; var originalDeactivate = computed.deactivate; computed.deactivate = function (close) { if (close) { return system.defer(function (dfd) { var list = items(); var results = 0; var listLength = list.length; function doDeactivate(item) { setTimeout(function () { computed.deactivateItem(item, close).then(function () { results++; items.remove(item); if (results == listLength) { dfd.resolve(); } }); }, 1); } for (var i = 0; i < listLength; i++) { doDeactivate(list[i]); } }).promise(); } else { return originalDeactivate(); } }; return computed; }; return computed; } /** * @class ActivatorSettings * @static */ var activatorSettings = { /** * The default value passed to an object's deactivate function as its close parameter. * @property {boolean} closeOnDeactivate * @default true */ closeOnDeactivate: true, /** * Lower-cased words which represent a truthy value. * @property {string[]} affirmations * @default ['yes', 'ok', 'true'] */ affirmations: ['yes', 'ok', 'true'], /** * Interprets the response of a `canActivate` or `canDeactivate` call using the known affirmative values in the `affirmations` array. * @method interpretResponse * @param {object} value * @return {boolean} */ interpretResponse: function(value) { if(system.isObject(value)) { value = value.can || false; } if(system.isString(value)) { return ko.utils.arrayIndexOf(this.affirmations, value.toLowerCase()) !== -1; } return value; }, /** * Determines whether or not the current item and the new item are the same. * @method areSameItem * @param {object} currentItem * @param {object} newItem * @param {object} currentActivationData * @param {object} newActivationData * @return {boolean} */ areSameItem: function(currentItem, newItem, currentActivationData, newActivationData) { return currentItem == newItem; }, /** * Called immediately before the new item is activated. * @method beforeActivate * @param {object} newItem */ beforeActivate: function(newItem) { return newItem; }, /** * Called immediately after the old item is deactivated. * @method afterDeactivate * @param {object} oldItem The previous item. * @param {boolean} close Whether or not the previous item was closed. * @param {function} setter The activate item setter function. */ afterDeactivate: function(oldItem, close, setter) { if(close && setter) { setter(null); } }, findChildActivator: function(item){ return null; } }; /** * @class ActivatorModule * @static */ activator = { /** * The default settings used by activators. * @property {ActivatorSettings} defaults */ defaults: activatorSettings, /** * Creates a new activator. * @method create * @param {object} [initialActiveItem] The item which should be immediately activated upon creation of the ativator. * @param {ActivatorSettings} [settings] Per activator overrides of the default activator settings. * @return {Activator} The created activator. */ create: createActivator, /** * Determines whether or not the provided object is an activator or not. * @method isActivator * @param {object} object Any object you wish to verify as an activator or not. * @return {boolean} True if the object is an activator; false otherwise. */ isActivator:function(object){ return object && object.__activator__; } }; return activator; });