activator.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. /**
  2. * Durandal 2.1.0 Copyright (c) 2012 Blue Spire Consulting, Inc. All Rights Reserved.
  3. * Available via the MIT license.
  4. * see: http://durandaljs.com or https://github.com/BlueSpire/Durandal for details.
  5. */
  6. /**
  7. * The activator module encapsulates all logic related to screen/component activation.
  8. * An activator is essentially an asynchronous state machine that understands a particular state transition protocol.
  9. * The protocol ensures that the following series of events always occur: `canDeactivate` (previous state), `canActivate` (new state), `deactivate` (previous state), `activate` (new state).
  10. * 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.
  11. * @module activator
  12. * @requires system
  13. * @requires knockout
  14. */
  15. define(['durandal/system', 'knockout'], function (system, ko) {
  16. var activator;
  17. var defaultOptions = {
  18. canDeactivate:true
  19. };
  20. function ensureSettings(settings) {
  21. if (settings == undefined) {
  22. settings = {};
  23. }
  24. if (!system.isBoolean(settings.closeOnDeactivate)) {
  25. settings.closeOnDeactivate = activator.defaults.closeOnDeactivate;
  26. }
  27. if (!settings.beforeActivate) {
  28. settings.beforeActivate = activator.defaults.beforeActivate;
  29. }
  30. if (!settings.afterDeactivate) {
  31. settings.afterDeactivate = activator.defaults.afterDeactivate;
  32. }
  33. if(!settings.affirmations){
  34. settings.affirmations = activator.defaults.affirmations;
  35. }
  36. if (!settings.interpretResponse) {
  37. settings.interpretResponse = activator.defaults.interpretResponse;
  38. }
  39. if (!settings.areSameItem) {
  40. settings.areSameItem = activator.defaults.areSameItem;
  41. }
  42. if (!settings.findChildActivator) {
  43. settings.findChildActivator = activator.defaults.findChildActivator;
  44. }
  45. return settings;
  46. }
  47. function invoke(target, method, data) {
  48. if (system.isArray(data)) {
  49. return target[method].apply(target, data);
  50. }
  51. return target[method](data);
  52. }
  53. function deactivate(item, close, settings, dfd, setter) {
  54. if (item && item.deactivate) {
  55. system.log('Deactivating', item);
  56. var result;
  57. try {
  58. result = item.deactivate(close);
  59. } catch(error) {
  60. system.log('ERROR: ' + error.message, error);
  61. dfd.resolve(false);
  62. return;
  63. }
  64. if (result && result.then) {
  65. result.then(function() {
  66. settings.afterDeactivate(item, close, setter);
  67. dfd.resolve(true);
  68. }, function(reason) {
  69. system.log(reason);
  70. dfd.resolve(false);
  71. });
  72. } else {
  73. settings.afterDeactivate(item, close, setter);
  74. dfd.resolve(true);
  75. }
  76. } else {
  77. if (item) {
  78. settings.afterDeactivate(item, close, setter);
  79. }
  80. dfd.resolve(true);
  81. }
  82. }
  83. function activate(newItem, activeItem, callback, activationData) {
  84. var result;
  85. if(newItem && newItem.activate) {
  86. system.log('Activating', newItem);
  87. try {
  88. result = invoke(newItem, 'activate', activationData);
  89. } catch(error) {
  90. system.log('ERROR: ' + error.message, error);
  91. callback(false);
  92. return;
  93. }
  94. }
  95. if(result && result.then) {
  96. result.then(function() {
  97. activeItem(newItem);
  98. callback(true);
  99. }, function(reason) {
  100. system.log('ERROR: ' + reason.message, reason);
  101. callback(false);
  102. });
  103. } else {
  104. activeItem(newItem);
  105. callback(true);
  106. }
  107. }
  108. function canDeactivateItem(item, close, settings, options) {
  109. options = system.extend({}, defaultOptions, options);
  110. settings.lifecycleData = null;
  111. return system.defer(function (dfd) {
  112. function continueCanDeactivate() {
  113. if (item && item.canDeactivate && options.canDeactivate) {
  114. var resultOrPromise;
  115. try {
  116. resultOrPromise = item.canDeactivate(close);
  117. } catch (error) {
  118. system.log('ERROR: ' + error.message, error);
  119. dfd.resolve(false);
  120. return;
  121. }
  122. if (resultOrPromise.then) {
  123. resultOrPromise.then(function (result) {
  124. settings.lifecycleData = result;
  125. dfd.resolve(settings.interpretResponse(result));
  126. }, function (reason) {
  127. system.log('ERROR: ' + reason.message, reason);
  128. dfd.resolve(false);
  129. });
  130. } else {
  131. settings.lifecycleData = resultOrPromise;
  132. dfd.resolve(settings.interpretResponse(resultOrPromise));
  133. }
  134. } else {
  135. dfd.resolve(true);
  136. }
  137. }
  138. var childActivator = settings.findChildActivator(item);
  139. if (childActivator) {
  140. childActivator.canDeactivate().then(function(result) {
  141. if (result) {
  142. continueCanDeactivate();
  143. } else {
  144. dfd.resolve(false);
  145. }
  146. });
  147. } else {
  148. continueCanDeactivate();
  149. }
  150. }).promise();
  151. };
  152. function canActivateItem(newItem, activeItem, settings, activeData, newActivationData) {
  153. settings.lifecycleData = null;
  154. return system.defer(function (dfd) {
  155. if (settings.areSameItem(activeItem(), newItem, activeData, newActivationData)) {
  156. dfd.resolve(true);
  157. return;
  158. }
  159. if (newItem && newItem.canActivate) {
  160. var resultOrPromise;
  161. try {
  162. resultOrPromise = invoke(newItem, 'canActivate', newActivationData);
  163. } catch (error) {
  164. system.log('ERROR: ' + error.message, error);
  165. dfd.resolve(false);
  166. return;
  167. }
  168. if (resultOrPromise.then) {
  169. resultOrPromise.then(function(result) {
  170. settings.lifecycleData = result;
  171. dfd.resolve(settings.interpretResponse(result));
  172. }, function(reason) {
  173. system.log('ERROR: ' + reason.message, reason);
  174. dfd.resolve(false);
  175. });
  176. } else {
  177. settings.lifecycleData = resultOrPromise;
  178. dfd.resolve(settings.interpretResponse(resultOrPromise));
  179. }
  180. } else {
  181. dfd.resolve(true);
  182. }
  183. }).promise();
  184. };
  185. /**
  186. * An activator is a read/write computed observable that enforces the activation lifecycle whenever changing values.
  187. * @class Activator
  188. */
  189. function createActivator(initialActiveItem, settings) {
  190. var activeItem = ko.observable(null);
  191. var activeData;
  192. settings = ensureSettings(settings);
  193. var computed = ko.computed({
  194. read: function () {
  195. return activeItem();
  196. },
  197. write: function (newValue) {
  198. computed.viaSetter = true;
  199. computed.activateItem(newValue);
  200. }
  201. });
  202. computed.__activator__ = true;
  203. /**
  204. * The settings for this activator.
  205. * @property {ActivatorSettings} settings
  206. */
  207. computed.settings = settings;
  208. settings.activator = computed;
  209. /**
  210. * An observable which indicates whether or not the activator is currently in the process of activating an instance.
  211. * @method isActivating
  212. * @return {boolean}
  213. */
  214. computed.isActivating = ko.observable(false);
  215. computed.forceActiveItem = function (item) {
  216. activeItem(item);
  217. };
  218. /**
  219. * Determines whether or not the specified item can be deactivated.
  220. * @method canDeactivateItem
  221. * @param {object} item The item to check.
  222. * @param {boolean} close Whether or not to check if close is possible.
  223. * @param {object} options Options for controlling the activation process.
  224. * @return {promise}
  225. */
  226. computed.canDeactivateItem = function (item, close, options) {
  227. return canDeactivateItem(item, close, settings, options);
  228. };
  229. /**
  230. * Deactivates the specified item.
  231. * @method deactivateItem
  232. * @param {object} item The item to deactivate.
  233. * @param {boolean} close Whether or not to close the item.
  234. * @return {promise}
  235. */
  236. computed.deactivateItem = function (item, close) {
  237. return system.defer(function(dfd) {
  238. computed.canDeactivateItem(item, close).then(function(canDeactivate) {
  239. if (canDeactivate) {
  240. deactivate(item, close, settings, dfd, activeItem);
  241. } else {
  242. computed.notifySubscribers();
  243. dfd.resolve(false);
  244. }
  245. });
  246. }).promise();
  247. };
  248. /**
  249. * Determines whether or not the specified item can be activated.
  250. * @method canActivateItem
  251. * @param {object} item The item to check.
  252. * @param {object} activationData Data associated with the activation.
  253. * @return {promise}
  254. */
  255. computed.canActivateItem = function (newItem, activationData) {
  256. return canActivateItem(newItem, activeItem, settings, activeData, activationData);
  257. };
  258. /**
  259. * Activates the specified item.
  260. * @method activateItem
  261. * @param {object} newItem The item to activate.
  262. * @param {object} newActivationData Data associated with the activation.
  263. * @param {object} options Options for controlling the activation process.
  264. * @return {promise}
  265. */
  266. computed.activateItem = function (newItem, newActivationData, options) {
  267. var viaSetter = computed.viaSetter;
  268. computed.viaSetter = false;
  269. return system.defer(function (dfd) {
  270. if (computed.isActivating()) {
  271. dfd.resolve(false);
  272. return;
  273. }
  274. computed.isActivating(true);
  275. var currentItem = activeItem();
  276. if (settings.areSameItem(currentItem, newItem, activeData, newActivationData)) {
  277. computed.isActivating(false);
  278. dfd.resolve(true);
  279. return;
  280. }
  281. computed.canDeactivateItem(currentItem, settings.closeOnDeactivate, options).then(function (canDeactivate) {
  282. if (canDeactivate) {
  283. computed.canActivateItem(newItem, newActivationData).then(function (canActivate) {
  284. if (canActivate) {
  285. system.defer(function (dfd2) {
  286. deactivate(currentItem, settings.closeOnDeactivate, settings, dfd2);
  287. }).promise().then(function () {
  288. newItem = settings.beforeActivate(newItem, newActivationData);
  289. activate(newItem, activeItem, function (result) {
  290. activeData = newActivationData;
  291. computed.isActivating(false);
  292. dfd.resolve(result);
  293. }, newActivationData);
  294. });
  295. } else {
  296. if (viaSetter) {
  297. computed.notifySubscribers();
  298. }
  299. computed.isActivating(false);
  300. dfd.resolve(false);
  301. }
  302. });
  303. } else {
  304. if (viaSetter) {
  305. computed.notifySubscribers();
  306. }
  307. computed.isActivating(false);
  308. dfd.resolve(false);
  309. }
  310. });
  311. }).promise();
  312. };
  313. /**
  314. * Determines whether or not the activator, in its current state, can be activated.
  315. * @method canActivate
  316. * @return {promise}
  317. */
  318. computed.canActivate = function () {
  319. var toCheck;
  320. if (initialActiveItem) {
  321. toCheck = initialActiveItem;
  322. initialActiveItem = false;
  323. } else {
  324. toCheck = computed();
  325. }
  326. return computed.canActivateItem(toCheck);
  327. };
  328. /**
  329. * Activates the activator, in its current state.
  330. * @method activate
  331. * @return {promise}
  332. */
  333. computed.activate = function () {
  334. var toActivate;
  335. if (initialActiveItem) {
  336. toActivate = initialActiveItem;
  337. initialActiveItem = false;
  338. } else {
  339. toActivate = computed();
  340. }
  341. return computed.activateItem(toActivate);
  342. };
  343. /**
  344. * Determines whether or not the activator, in its current state, can be deactivated.
  345. * @method canDeactivate
  346. * @return {promise}
  347. */
  348. computed.canDeactivate = function (close) {
  349. return computed.canDeactivateItem(computed(), close);
  350. };
  351. /**
  352. * Deactivates the activator, in its current state.
  353. * @method deactivate
  354. * @return {promise}
  355. */
  356. computed.deactivate = function (close) {
  357. return computed.deactivateItem(computed(), close);
  358. };
  359. computed.includeIn = function (includeIn) {
  360. includeIn.canActivate = function () {
  361. return computed.canActivate();
  362. };
  363. includeIn.activate = function () {
  364. return computed.activate();
  365. };
  366. includeIn.canDeactivate = function (close) {
  367. return computed.canDeactivate(close);
  368. };
  369. includeIn.deactivate = function (close) {
  370. return computed.deactivate(close);
  371. };
  372. };
  373. if (settings.includeIn) {
  374. computed.includeIn(settings.includeIn);
  375. } else if (initialActiveItem) {
  376. computed.activate();
  377. }
  378. computed.forItems = function (items) {
  379. settings.closeOnDeactivate = false;
  380. settings.determineNextItemToActivate = function (list, lastIndex) {
  381. var toRemoveAt = lastIndex - 1;
  382. if (toRemoveAt == -1 && list.length > 1) {
  383. return list[1];
  384. }
  385. if (toRemoveAt > -1 && toRemoveAt < list.length - 1) {
  386. return list[toRemoveAt];
  387. }
  388. return null;
  389. };
  390. settings.beforeActivate = function (newItem) {
  391. var currentItem = computed();
  392. if (!newItem) {
  393. newItem = settings.determineNextItemToActivate(items, currentItem ? items.indexOf(currentItem) : 0);
  394. } else {
  395. var index = items.indexOf(newItem);
  396. if (index == -1) {
  397. items.push(newItem);
  398. } else {
  399. newItem = items()[index];
  400. }
  401. }
  402. return newItem;
  403. };
  404. settings.afterDeactivate = function (oldItem, close) {
  405. if (close) {
  406. items.remove(oldItem);
  407. }
  408. };
  409. var originalCanDeactivate = computed.canDeactivate;
  410. computed.canDeactivate = function (close) {
  411. if (close) {
  412. return system.defer(function (dfd) {
  413. var list = items();
  414. var results = [];
  415. function finish() {
  416. for (var j = 0; j < results.length; j++) {
  417. if (!results[j]) {
  418. dfd.resolve(false);
  419. return;
  420. }
  421. }
  422. dfd.resolve(true);
  423. }
  424. for (var i = 0; i < list.length; i++) {
  425. computed.canDeactivateItem(list[i], close).then(function (result) {
  426. results.push(result);
  427. if (results.length == list.length) {
  428. finish();
  429. }
  430. });
  431. }
  432. }).promise();
  433. } else {
  434. return originalCanDeactivate();
  435. }
  436. };
  437. var originalDeactivate = computed.deactivate;
  438. computed.deactivate = function (close) {
  439. if (close) {
  440. return system.defer(function (dfd) {
  441. var list = items();
  442. var results = 0;
  443. var listLength = list.length;
  444. function doDeactivate(item) {
  445. setTimeout(function () {
  446. computed.deactivateItem(item, close).then(function () {
  447. results++;
  448. items.remove(item);
  449. if (results == listLength) {
  450. dfd.resolve();
  451. }
  452. });
  453. }, 1);
  454. }
  455. for (var i = 0; i < listLength; i++) {
  456. doDeactivate(list[i]);
  457. }
  458. }).promise();
  459. } else {
  460. return originalDeactivate();
  461. }
  462. };
  463. return computed;
  464. };
  465. return computed;
  466. }
  467. /**
  468. * @class ActivatorSettings
  469. * @static
  470. */
  471. var activatorSettings = {
  472. /**
  473. * The default value passed to an object's deactivate function as its close parameter.
  474. * @property {boolean} closeOnDeactivate
  475. * @default true
  476. */
  477. closeOnDeactivate: true,
  478. /**
  479. * Lower-cased words which represent a truthy value.
  480. * @property {string[]} affirmations
  481. * @default ['yes', 'ok', 'true']
  482. */
  483. affirmations: ['yes', 'ok', 'true'],
  484. /**
  485. * Interprets the response of a `canActivate` or `canDeactivate` call using the known affirmative values in the `affirmations` array.
  486. * @method interpretResponse
  487. * @param {object} value
  488. * @return {boolean}
  489. */
  490. interpretResponse: function(value) {
  491. if(system.isObject(value)) {
  492. value = value.can || false;
  493. }
  494. if(system.isString(value)) {
  495. return ko.utils.arrayIndexOf(this.affirmations, value.toLowerCase()) !== -1;
  496. }
  497. return value;
  498. },
  499. /**
  500. * Determines whether or not the current item and the new item are the same.
  501. * @method areSameItem
  502. * @param {object} currentItem
  503. * @param {object} newItem
  504. * @param {object} currentActivationData
  505. * @param {object} newActivationData
  506. * @return {boolean}
  507. */
  508. areSameItem: function(currentItem, newItem, currentActivationData, newActivationData) {
  509. return currentItem == newItem;
  510. },
  511. /**
  512. * Called immediately before the new item is activated.
  513. * @method beforeActivate
  514. * @param {object} newItem
  515. */
  516. beforeActivate: function(newItem) {
  517. return newItem;
  518. },
  519. /**
  520. * Called immediately after the old item is deactivated.
  521. * @method afterDeactivate
  522. * @param {object} oldItem The previous item.
  523. * @param {boolean} close Whether or not the previous item was closed.
  524. * @param {function} setter The activate item setter function.
  525. */
  526. afterDeactivate: function(oldItem, close, setter) {
  527. if(close && setter) {
  528. setter(null);
  529. }
  530. },
  531. findChildActivator: function(item){
  532. return null;
  533. }
  534. };
  535. /**
  536. * @class ActivatorModule
  537. * @static
  538. */
  539. activator = {
  540. /**
  541. * The default settings used by activators.
  542. * @property {ActivatorSettings} defaults
  543. */
  544. defaults: activatorSettings,
  545. /**
  546. * Creates a new activator.
  547. * @method create
  548. * @param {object} [initialActiveItem] The item which should be immediately activated upon creation of the ativator.
  549. * @param {ActivatorSettings} [settings] Per activator overrides of the default activator settings.
  550. * @return {Activator} The created activator.
  551. */
  552. create: createActivator,
  553. /**
  554. * Determines whether or not the provided object is an activator or not.
  555. * @method isActivator
  556. * @param {object} object Any object you wish to verify as an activator or not.
  557. * @return {boolean} True if the object is an activator; false otherwise.
  558. */
  559. isActivator:function(object){
  560. return object && object.__activator__;
  561. }
  562. };
  563. return activator;
  564. });