/***************************************************************************** * Open MCT Web, Copyright (c) 2014-2015, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * * Open MCT Web is licensed under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0. * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. * * Open MCT Web includes source code licensed under additional open source * licenses. See the Open Source Licenses file (LICENSES.md) included with * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ /*global define,Promise*/ /** * This bundle implements the directives for representing domain objects * as Angular-managed HTML. * @namespace platform/representation */ define( [], function () { "use strict"; /** * Defines the mct-representation directive. This may be used to * present domain objects as HTML (with event wiring), with the * specific representation being mapped to a defined extension * (as defined in either the `representation` category-of-extension, * or the `views` category-of-extension.) * * This directive uses two-way binding for three attributes: * * * `key`, matched against the key of a defined template extension * in order to determine which actual template to include. * * `mct-object`, populated as `domainObject` in the loaded * template's scope. This is the domain object being * represented as HTML by this directive. * * `parameters`, used to communicate display parameters to * the included template (e.g. title.) * * @memberof platform/representation * @constructor * @param {RepresentationDefinition[]} representations an array of * representation extensions * @param {ViewDefinition[]} views an array of view extensions */ function MCTRepresentation(representations, views, representers, $q, templateLinker, $log) { var representationMap = {}, gestureMap = {}; // Assemble all representations and views // The distinction between views and representations is // not important here (view is-a representation) representations.concat(views).forEach(function (representation) { var key = representation.key; // Store the representation representationMap[key] = representationMap[key] || []; representationMap[representation.key].push(representation); }); // Get a path to a representation function getPath(representation) { return templateLinker.getPath(representation); } // Look up a matching representation for this domain object function lookup(key, domainObject) { var candidates = representationMap[key] || [], type, i; // Filter candidates by object type for (i = 0; i < candidates.length; i += 1) { type = candidates[i].type; if (!type || !domainObject || domainObject.getCapability('type').instanceOf(type)) { return candidates[i]; } } } function link($scope, element, attrs, ctrl, transclude) { var activeRepresenters = representers.map(function (Representer) { return new Representer($scope, element, attrs); }), toClear = [], // Properties to clear out of scope on change counter = 0, couldRepresent = false, lastIdPath = [], lastKey, changeTemplate = templateLinker.link($scope, element); // Populate scope with any capabilities indicated by the // representation's extension definition function refreshCapabilities() { var domainObject = $scope.domainObject, representation = lookup($scope.key, domainObject), uses = ((representation || {}).uses || []), myCounter = counter; if (domainObject) { // Update model $scope.model = domainObject.getModel(); // Provide any of the capabilities requested uses.forEach(function (used) { $log.debug([ "Requesting capability ", used, " for representation ", $scope.key ].join("")); $q.when( domainObject.useCapability(used) ).then(function (c) { // Avoid clobbering capabilities from // subsequent representations; // Angular reuses scopes. if (counter === myCounter) { $scope[used] = c; } }); }); } } // Destroy (deallocate any resources associated with) any // active representers. function destroyRepresenters() { activeRepresenters.forEach(function (activeRepresenter) { activeRepresenter.destroy(); }); } function unchanged(canRepresent, idPath, key) { return canRepresent && couldRepresent && key === lastKey && idPath.length === lastIdPath.length && idPath.every(function (id, i) { return id === lastIdPath[i]; }); } function getIdPath(domainObject) { if (!domainObject) { return []; } if (!domainObject.hasCapability('context')) { return [domainObject.getId()]; } return domainObject.getCapability('context') .getPath().map(function (pathObject) { return pathObject.getId(); }); } // General-purpose refresh mechanism; should set up the scope // as appropriate for current representation key and // domain object. function refresh() { var domainObject = $scope.domainObject, representation = lookup($scope.key, domainObject), path = representation && getPath(representation), uses = ((representation || {}).uses || []), canRepresent = !!(path && domainObject), idPath = getIdPath(domainObject), key = $scope.key; if (unchanged(canRepresent, idPath, key)) { return; } // Create an empty object named "representation", for this // representation to store local variables into. $scope.representation = {}; // Change templates (passing in undefined to clear // if we don't have enough info to show a template.) changeTemplate(canRepresent ? path : undefined); // Any existing representers are no longer valid; release them. destroyRepresenters(); // Log if a key was given, but no matching representation // was found. if (!representation && $scope.key) { $log.warn("No representation found for " + $scope.key); } // Clear out the scope from the last representation toClear.forEach(function (property) { delete $scope[property]; }); // To allow simplified change detection next time around couldRepresent = canRepresent; lastIdPath = idPath; lastKey = key; // Populate scope with fields associated with the current // domain object (if one has been passed in) if (canRepresent) { // Track how many representations we've made in this scope, // to ensure that the correct representations are matched to // the correct object/key pairs. counter += 1; // Initialize any capabilities refreshCapabilities(); // Also provide the view configuration, // for the specific view $scope.configuration = ($scope.model.configuration || {})[$scope.key] || {}; // Finally, wire up any additional behavior (such as // gestures) associated with this representation. activeRepresenters.forEach(function (representer) { representer.represent(representation, domainObject); }); // Track which properties we want to clear from scope // next change object/key pair changes toClear = uses.concat(['model']); } } // Update the representation when the key changes (e.g. if a // different representation has been selected) $scope.$watch("key", refresh); // Also update when the represented domain object changes // (to a different object) $scope.$watch("domainObject", refresh); // Finally, also update when there is a new version of that // same domain object; these changes should be tracked in the // model's "modified" field, by the mutation capability. $scope.$watch("domainObject.getModel().modified", refreshCapabilities); // Make sure any resources allocated by representers also get // released. $scope.$on("$destroy", destroyRepresenters); // Do one initial refresh, so that we don't need another // digest iteration just to populate the scope. Failure to // do this can result in unstable digest cycles, which // Angular will detect, and throw an Error about. refresh(); } return { // Only applicable at the element level restrict: "E", // Handle Angular's linking step link: link, // May hide the element, so let other directives act first priority: -1000, // Two-way bind key and parameters, get the represented domain // object as "mct-object" scope: { key: "=", domainObject: "=mctObject", ngModel: "=", parameters: "=" } }; } /** * A representer participates in the process of instantiating a * representation of a domain object. * * @interface Representer * @augments {Destroyable} */ /** * Set the current representation in use, and the domain * object being represented. * * @method Representer#represent * @param {RepresentationDefinition} representation the * definition of the representation in use * @param {DomainObject} domainObject the domain object * being represented */ return MCTRepresentation; } );