UNPKG

durandal

Version:

Durandal is a cross-device, cross-platform client framework written in JavaScript and designed to make Single Page Applications (SPAs) easy to create and maintain. We've used it to build apps for PC, Mac, Linux, iOS and Android...and now it's your turn...

658 lines (572 loc) 23.7 kB
/** * Durandal 2.2.0 Copyright (c) 2010-2016 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) { if (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) { if (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) { if (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) { if (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; });