composition.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  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 composition module encapsulates all functionality related to visual composition.
  8. * @module composition
  9. * @requires system
  10. * @requires viewLocator
  11. * @requires binder
  12. * @requires viewEngine
  13. * @requires activator
  14. * @requires jquery
  15. * @requires knockout
  16. */
  17. define(['durandal/system', 'durandal/viewLocator', 'durandal/binder', 'durandal/viewEngine', 'durandal/activator', 'jquery', 'knockout'], function (system, viewLocator, binder, viewEngine, activator, $, ko) {
  18. var dummyModel = {},
  19. activeViewAttributeName = 'data-active-view',
  20. composition,
  21. compositionCompleteCallbacks = [],
  22. compositionCount = 0,
  23. compositionDataKey = 'durandal-composition-data',
  24. partAttributeName = 'data-part',
  25. bindableSettings = ['model', 'view', 'transition', 'area', 'strategy', 'activationData', 'onError'],
  26. visibilityKey = "durandal-visibility-data",
  27. composeBindings = ['compose:'];
  28. function onError(context, error, element) {
  29. try {
  30. if (context.onError) {
  31. try {
  32. context.onError(error, element);
  33. } catch (e) {
  34. system.error(e);
  35. }
  36. } else {
  37. system.error(error);
  38. }
  39. } finally {
  40. endComposition(context, element, true);
  41. }
  42. }
  43. function getHostState(parent) {
  44. var elements = [];
  45. var state = {
  46. childElements: elements,
  47. activeView: null
  48. };
  49. var child = ko.virtualElements.firstChild(parent);
  50. while (child) {
  51. if (child.nodeType == 1) {
  52. elements.push(child);
  53. if (child.getAttribute(activeViewAttributeName)) {
  54. state.activeView = child;
  55. }
  56. }
  57. child = ko.virtualElements.nextSibling(child);
  58. }
  59. if(!state.activeView){
  60. state.activeView = elements[0];
  61. }
  62. return state;
  63. }
  64. function endComposition(context, element, error) {
  65. compositionCount--;
  66. if(compositionCount === 0) {
  67. var callBacks = compositionCompleteCallbacks;
  68. compositionCompleteCallbacks = [];
  69. if (!error) {
  70. setTimeout(function () {
  71. var i = callBacks.length;
  72. while (i--) {
  73. try {
  74. callBacks[i]();
  75. } catch (e) {
  76. onError(context, e, element);
  77. }
  78. }
  79. }, 1);
  80. }
  81. }
  82. cleanUp(context);
  83. }
  84. function cleanUp(context){
  85. delete context.activeView;
  86. delete context.viewElements;
  87. }
  88. function tryActivate(context, successCallback, skipActivation, element) {
  89. if(skipActivation){
  90. successCallback();
  91. } else if (context.activate && context.model && context.model.activate) {
  92. var result;
  93. try{
  94. if(system.isArray(context.activationData)) {
  95. result = context.model.activate.apply(context.model, context.activationData);
  96. } else {
  97. result = context.model.activate(context.activationData);
  98. }
  99. if(result && result.then) {
  100. result.then(successCallback, function(reason) {
  101. onError(context, reason, element);
  102. successCallback();
  103. });
  104. } else if(result || result === undefined) {
  105. successCallback();
  106. } else {
  107. endComposition(context, element);
  108. }
  109. }
  110. catch(e){
  111. onError(context, e, element);
  112. }
  113. } else {
  114. successCallback();
  115. }
  116. }
  117. function triggerAttach(context, element) {
  118. var context = this;
  119. if (context.activeView) {
  120. context.activeView.removeAttribute(activeViewAttributeName);
  121. }
  122. if (context.child) {
  123. try{
  124. if (context.model && context.model.attached) {
  125. if (context.composingNewView || context.alwaysTriggerAttach) {
  126. context.model.attached(context.child, context.parent, context);
  127. }
  128. }
  129. if (context.attached) {
  130. context.attached(context.child, context.parent, context);
  131. }
  132. context.child.setAttribute(activeViewAttributeName, true);
  133. if (context.composingNewView && context.model && context.model.detached) {
  134. ko.utils.domNodeDisposal.addDisposeCallback(context.child, function () {
  135. try{
  136. context.model.detached(context.child, context.parent, context);
  137. }catch(e2){
  138. onError(context, e2, element);
  139. }
  140. });
  141. }
  142. }catch(e){
  143. onError(context, e, element);
  144. }
  145. }
  146. context.triggerAttach = system.noop;
  147. }
  148. function shouldTransition(context) {
  149. if (system.isString(context.transition)) {
  150. if (context.activeView) {
  151. if (context.activeView == context.child) {
  152. return false;
  153. }
  154. if (!context.child) {
  155. return true;
  156. }
  157. if (context.skipTransitionOnSameViewId) {
  158. var currentViewId = context.activeView.getAttribute('data-view');
  159. var newViewId = context.child.getAttribute('data-view');
  160. return currentViewId != newViewId;
  161. }
  162. }
  163. return true;
  164. }
  165. return false;
  166. }
  167. function cloneNodes(nodesArray) {
  168. for (var i = 0, j = nodesArray.length, newNodesArray = []; i < j; i++) {
  169. var clonedNode = nodesArray[i].cloneNode(true);
  170. newNodesArray.push(clonedNode);
  171. }
  172. return newNodesArray;
  173. }
  174. function replaceParts(context){
  175. var parts = cloneNodes(context.parts);
  176. var replacementParts = composition.getParts(parts);
  177. var standardParts = composition.getParts(context.child);
  178. for (var partId in replacementParts) {
  179. var toReplace = standardParts[partId];
  180. if (!toReplace) {
  181. toReplace = $('[data-part="' + partId + '"]', context.child).get(0);
  182. if (!toReplace) {
  183. system.log('Could not find part to override: ' + partId);
  184. continue;
  185. }
  186. }
  187. toReplace.parentNode.replaceChild(replacementParts[partId], toReplace);
  188. }
  189. }
  190. function removePreviousView(context){
  191. var children = ko.virtualElements.childNodes(context.parent), i, len;
  192. if(!system.isArray(children)){
  193. var arrayChildren = [];
  194. for(i = 0, len = children.length; i < len; i++){
  195. arrayChildren[i] = children[i];
  196. }
  197. children = arrayChildren;
  198. }
  199. for(i = 1,len = children.length; i < len; i++){
  200. ko.removeNode(children[i]);
  201. }
  202. }
  203. function hide(view) {
  204. ko.utils.domData.set(view, visibilityKey, view.style.display);
  205. view.style.display = 'none';
  206. }
  207. function show(view) {
  208. var displayStyle = ko.utils.domData.get(view, visibilityKey);
  209. view.style.display = displayStyle === 'none' ? 'block' : displayStyle;
  210. }
  211. function hasComposition(element){
  212. var dataBind = element.getAttribute('data-bind');
  213. if(!dataBind){
  214. return false;
  215. }
  216. for(var i = 0, length = composeBindings.length; i < length; i++){
  217. if(dataBind.indexOf(composeBindings[i]) > -1){
  218. return true;
  219. }
  220. }
  221. return false;
  222. }
  223. /**
  224. * @class CompositionTransaction
  225. * @static
  226. */
  227. var compositionTransaction = {
  228. /**
  229. * Registers a callback which will be invoked when the current composition transaction has completed. The transaction includes all parent and children compositions.
  230. * @method complete
  231. * @param {function} callback The callback to be invoked when composition is complete.
  232. */
  233. complete: function (callback) {
  234. compositionCompleteCallbacks.push(callback);
  235. }
  236. };
  237. /**
  238. * @class CompositionModule
  239. * @static
  240. */
  241. composition = {
  242. /**
  243. * An array of all the binding handler names (includeing :) that trigger a composition.
  244. * @property {string} composeBindings
  245. * @default ['compose:']
  246. */
  247. composeBindings:composeBindings,
  248. /**
  249. * Converts a transition name to its moduleId.
  250. * @method convertTransitionToModuleId
  251. * @param {string} name The name of the transtion.
  252. * @return {string} The moduleId.
  253. */
  254. convertTransitionToModuleId: function (name) {
  255. return 'transitions/' + name;
  256. },
  257. /**
  258. * The name of the transition to use in all compositions.
  259. * @property {string} defaultTransitionName
  260. * @default null
  261. */
  262. defaultTransitionName: null,
  263. /**
  264. * Represents the currently executing composition transaction.
  265. * @property {CompositionTransaction} current
  266. */
  267. current: compositionTransaction,
  268. /**
  269. * Registers a binding handler that will be invoked when the current composition transaction is complete.
  270. * @method addBindingHandler
  271. * @param {string} name The name of the binding handler.
  272. * @param {object} [config] The binding handler instance. If none is provided, the name will be used to look up an existing handler which will then be converted to a composition handler.
  273. * @param {function} [initOptionsFactory] If the registered binding needs to return options from its init call back to knockout, this function will server as a factory for those options. It will receive the same parameters that the init function does.
  274. */
  275. addBindingHandler:function(name, config, initOptionsFactory){
  276. var key,
  277. dataKey = 'composition-handler-' + name,
  278. handler;
  279. config = config || ko.bindingHandlers[name];
  280. initOptionsFactory = initOptionsFactory || function(){ return undefined; };
  281. handler = ko.bindingHandlers[name] = {
  282. init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
  283. if(compositionCount > 0){
  284. var data = {
  285. trigger:ko.observable(null)
  286. };
  287. composition.current.complete(function(){
  288. if(config.init){
  289. config.init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
  290. }
  291. if(config.update){
  292. ko.utils.domData.set(element, dataKey, config);
  293. data.trigger('trigger');
  294. }
  295. });
  296. ko.utils.domData.set(element, dataKey, data);
  297. }else{
  298. ko.utils.domData.set(element, dataKey, config);
  299. if(config.init){
  300. config.init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
  301. }
  302. }
  303. return initOptionsFactory(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
  304. },
  305. update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
  306. var data = ko.utils.domData.get(element, dataKey);
  307. if(data.update){
  308. return data.update(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
  309. }
  310. if(data.trigger){
  311. data.trigger();
  312. }
  313. }
  314. };
  315. for (key in config) {
  316. if (key !== "init" && key !== "update") {
  317. handler[key] = config[key];
  318. }
  319. }
  320. },
  321. /**
  322. * Gets an object keyed with all the elements that are replacable parts, found within the supplied elements. The key will be the part name and the value will be the element itself.
  323. * @method getParts
  324. * @param {DOMElement\DOMElement[]} elements The element(s) to search for parts.
  325. * @return {object} An object keyed by part.
  326. */
  327. getParts: function(elements, parts) {
  328. parts = parts || {};
  329. if (!elements) {
  330. return parts;
  331. }
  332. if (elements.length === undefined) {
  333. elements = [elements];
  334. }
  335. for (var i = 0, length = elements.length; i < length; i++) {
  336. var element = elements[i],
  337. id;
  338. if (element.getAttribute) {
  339. id = element.getAttribute(partAttributeName);
  340. if (id) {
  341. parts[id] = element;
  342. }
  343. if (element.hasChildNodes() && !hasComposition(element)) {
  344. composition.getParts(element.childNodes, parts);
  345. }
  346. }
  347. }
  348. return parts;
  349. },
  350. cloneNodes:cloneNodes,
  351. finalize: function (context, element) {
  352. if(context.transition === undefined) {
  353. context.transition = this.defaultTransitionName;
  354. }
  355. if(!context.child && !context.activeView){
  356. if (!context.cacheViews) {
  357. ko.virtualElements.emptyNode(context.parent);
  358. }
  359. context.triggerAttach(context, element);
  360. endComposition(context, element);
  361. } else if (shouldTransition(context)) {
  362. var transitionModuleId = this.convertTransitionToModuleId(context.transition);
  363. system.acquire(transitionModuleId).then(function (transition) {
  364. context.transition = transition;
  365. transition(context).then(function () {
  366. if (!context.cacheViews) {
  367. if(!context.child){
  368. ko.virtualElements.emptyNode(context.parent);
  369. }else{
  370. removePreviousView(context);
  371. }
  372. }else if(context.activeView){
  373. var instruction = binder.getBindingInstruction(context.activeView);
  374. if(instruction && instruction.cacheViews != undefined && !instruction.cacheViews){
  375. ko.removeNode(context.activeView);
  376. }else{
  377. hide(context.activeView);
  378. }
  379. }
  380. if (context.child) {
  381. show(context.child);
  382. }
  383. context.triggerAttach(context, element);
  384. endComposition(context, element);
  385. });
  386. }).fail(function(err){
  387. onError(context, 'Failed to load transition (' + transitionModuleId + '). Details: ' + err.message, element);
  388. });
  389. } else {
  390. if (context.child != context.activeView) {
  391. if (context.cacheViews && context.activeView) {
  392. var instruction = binder.getBindingInstruction(context.activeView);
  393. if(!instruction || (instruction.cacheViews != undefined && !instruction.cacheViews)){
  394. ko.removeNode(context.activeView);
  395. }else{
  396. hide(context.activeView);
  397. }
  398. }
  399. if (!context.child) {
  400. if (!context.cacheViews) {
  401. ko.virtualElements.emptyNode(context.parent);
  402. }
  403. } else {
  404. if (!context.cacheViews) {
  405. removePreviousView(context);
  406. }
  407. show(context.child);
  408. }
  409. }
  410. context.triggerAttach(context, element);
  411. endComposition(context, element);
  412. }
  413. },
  414. bindAndShow: function (child, element, context, skipActivation) {
  415. context.child = child;
  416. context.parent.__composition_context = context;
  417. if (context.cacheViews) {
  418. context.composingNewView = (ko.utils.arrayIndexOf(context.viewElements, child) == -1);
  419. } else {
  420. context.composingNewView = true;
  421. }
  422. tryActivate(context, function () {
  423. if (context.parent.__composition_context == context) {
  424. delete context.parent.__composition_context;
  425. if (context.binding) {
  426. context.binding(context.child, context.parent, context);
  427. }
  428. if (context.preserveContext && context.bindingContext) {
  429. if (context.composingNewView) {
  430. if(context.parts){
  431. replaceParts(context);
  432. }
  433. hide(child);
  434. ko.virtualElements.prepend(context.parent, child);
  435. binder.bindContext(context.bindingContext, child, context.model, context.as);
  436. }
  437. } else if (child) {
  438. var modelToBind = context.model || dummyModel;
  439. var currentModel = ko.dataFor(child);
  440. if (currentModel != modelToBind) {
  441. if (!context.composingNewView) {
  442. ko.removeNode(child);
  443. viewEngine.createView(child.getAttribute('data-view')).then(function(recreatedView) {
  444. composition.bindAndShow(recreatedView, element, context, true);
  445. });
  446. return;
  447. }
  448. if(context.parts){
  449. replaceParts(context);
  450. }
  451. hide(child);
  452. ko.virtualElements.prepend(context.parent, child);
  453. binder.bind(modelToBind, child);
  454. }
  455. }
  456. composition.finalize(context, element);
  457. } else {
  458. endComposition(context, element);
  459. }
  460. }, skipActivation, element);
  461. },
  462. /**
  463. * Eecutes the default view location strategy.
  464. * @method defaultStrategy
  465. * @param {object} context The composition context containing the model and possibly existing viewElements.
  466. * @return {promise} A promise for the view.
  467. */
  468. defaultStrategy: function (context) {
  469. return viewLocator.locateViewForObject(context.model, context.area, context.viewElements);
  470. },
  471. getSettings: function (valueAccessor, element) {
  472. var value = valueAccessor(),
  473. settings = ko.utils.unwrapObservable(value) || {},
  474. activatorPresent = activator.isActivator(value),
  475. moduleId;
  476. if (system.isString(settings)) {
  477. if (viewEngine.isViewUrl(settings)) {
  478. settings = {
  479. view: settings
  480. };
  481. } else {
  482. settings = {
  483. model: settings,
  484. activate: !activatorPresent
  485. };
  486. }
  487. return settings;
  488. }
  489. moduleId = system.getModuleId(settings);
  490. if (moduleId) {
  491. settings = {
  492. model: settings,
  493. activate: !activatorPresent
  494. };
  495. return settings;
  496. }
  497. if(!activatorPresent && settings.model) {
  498. activatorPresent = activator.isActivator(settings.model);
  499. }
  500. for (var attrName in settings) {
  501. if (ko.utils.arrayIndexOf(bindableSettings, attrName) != -1) {
  502. settings[attrName] = ko.utils.unwrapObservable(settings[attrName]);
  503. } else {
  504. settings[attrName] = settings[attrName];
  505. }
  506. }
  507. if (activatorPresent) {
  508. settings.activate = false;
  509. } else if (settings.activate === undefined) {
  510. settings.activate = true;
  511. }
  512. return settings;
  513. },
  514. executeStrategy: function (context, element) {
  515. context.strategy(context).then(function (child) {
  516. composition.bindAndShow(child, element, context);
  517. });
  518. },
  519. inject: function (context, element) {
  520. if (!context.model) {
  521. this.bindAndShow(null, element, context);
  522. return;
  523. }
  524. if (context.view) {
  525. viewLocator.locateView(context.view, context.area, context.viewElements).then(function (child) {
  526. composition.bindAndShow(child, element, context);
  527. });
  528. return;
  529. }
  530. if (!context.strategy) {
  531. context.strategy = this.defaultStrategy;
  532. }
  533. if (system.isString(context.strategy)) {
  534. system.acquire(context.strategy).then(function (strategy) {
  535. context.strategy = strategy;
  536. composition.executeStrategy(context, element);
  537. }).fail(function (err) {
  538. onError(context, 'Failed to load view strategy (' + context.strategy + '). Details: ' + err.message, element);
  539. });
  540. } else {
  541. this.executeStrategy(context, element);
  542. }
  543. },
  544. /**
  545. * Initiates a composition.
  546. * @method compose
  547. * @param {DOMElement} element The DOMElement or knockout virtual element that serves as the parent for the composition.
  548. * @param {object} settings The composition settings.
  549. * @param {object} [bindingContext] The current binding context.
  550. */
  551. compose: function (element, settings, bindingContext, fromBinding) {
  552. compositionCount++;
  553. if(!fromBinding){
  554. settings = composition.getSettings(function() { return settings; }, element);
  555. }
  556. if (settings.compositionComplete) {
  557. compositionCompleteCallbacks.push(function () {
  558. settings.compositionComplete(settings.child, settings.parent, settings);
  559. });
  560. }
  561. compositionCompleteCallbacks.push(function () {
  562. if(settings.composingNewView && settings.model && settings.model.compositionComplete){
  563. settings.model.compositionComplete(settings.child, settings.parent, settings);
  564. }
  565. });
  566. var hostState = getHostState(element);
  567. settings.activeView = hostState.activeView;
  568. settings.parent = element;
  569. settings.triggerAttach = triggerAttach;
  570. settings.bindingContext = bindingContext;
  571. if (settings.cacheViews && !settings.viewElements) {
  572. settings.viewElements = hostState.childElements;
  573. }
  574. if (!settings.model) {
  575. if (!settings.view) {
  576. this.bindAndShow(null, element, settings);
  577. } else {
  578. settings.area = settings.area || 'partial';
  579. settings.preserveContext = true;
  580. viewLocator.locateView(settings.view, settings.area, settings.viewElements).then(function (child) {
  581. composition.bindAndShow(child, element, settings);
  582. });
  583. }
  584. } else if (system.isString(settings.model)) {
  585. system.acquire(settings.model).then(function (module) {
  586. settings.model = system.resolveObject(module);
  587. composition.inject(settings, element);
  588. }).fail(function (err) {
  589. onError(settings, 'Failed to load composed module (' + settings.model + '). Details: ' + err.message, element);
  590. });
  591. } else {
  592. composition.inject(settings, element);
  593. }
  594. }
  595. };
  596. ko.bindingHandlers.compose = {
  597. init: function() {
  598. return { controlsDescendantBindings: true };
  599. },
  600. update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
  601. var settings = composition.getSettings(valueAccessor, element);
  602. if(settings.mode){
  603. var data = ko.utils.domData.get(element, compositionDataKey);
  604. if(!data){
  605. var childNodes = ko.virtualElements.childNodes(element);
  606. data = {};
  607. if(settings.mode === 'inline'){
  608. data.view = viewEngine.ensureSingleElement(childNodes);
  609. }else if(settings.mode === 'templated'){
  610. data.parts = cloneNodes(childNodes);
  611. }
  612. ko.virtualElements.emptyNode(element);
  613. ko.utils.domData.set(element, compositionDataKey, data);
  614. }
  615. if(settings.mode === 'inline'){
  616. settings.view = data.view.cloneNode(true);
  617. }else if(settings.mode === 'templated'){
  618. settings.parts = data.parts;
  619. }
  620. settings.preserveContext = true;
  621. }
  622. composition.compose(element, settings, bindingContext, true);
  623. }
  624. };
  625. ko.virtualElements.allowedBindings.compose = true;
  626. return composition;
  627. });