diff --git a/platform/representation/src/MCTInclude.js b/platform/representation/src/MCTInclude.js index ed68394dac..78ac424180 100644 --- a/platform/representation/src/MCTInclude.js +++ b/platform/representation/src/MCTInclude.js @@ -57,7 +57,8 @@ define( // Use the included controller to populate scope controller: controller, - // Use ng-include as a template; it gets the real template path + // Use ng-include as a template; "inclusion" will be the real + // template path template: '', // Two-way bind key, ngModel, and parameters diff --git a/platform/representation/src/MCTRepresentation.js b/platform/representation/src/MCTRepresentation.js index 3c6265dcb5..f266535a35 100644 --- a/platform/representation/src/MCTRepresentation.js +++ b/platform/representation/src/MCTRepresentation.js @@ -55,13 +55,21 @@ define( function link($scope, element) { var gestureHandle; + // General-purpose refresh mechanism; should set up the scope + // as appropriate for current representation key and + // domain object. function refresh() { var representation = representationMap[$scope.key], domainObject = $scope.domainObject, uses = ((representation || {}).uses || []), gestureKeys = ((representation || {}).gestures || []); + // Create an empty object named "representation", for this + // representation to store local variables into. $scope.representation = {}; + + // Look up the actual template path, pass it to ng-include + // via the "inclusion" field $scope.inclusion = pathMap[$scope.key]; // Any existing gestures are no longer valid; release them. @@ -69,12 +77,19 @@ define( gestureHandle.destroy(); } + // Log if a key was given, but no matching representation + // was found. if (!representation && $scope.key) { $log.warn("No representation found for " + $scope.key); } + + // Populate scope with fields associated with the current + // domain object (if one has been passed in) if (domainObject) { + // Always provide the model, as "model" $scope.model = domainObject.getModel(); + // Also provide any of the capabilities requested uses.forEach(function (used) { $log.debug([ "Requesting capability ", @@ -89,6 +104,8 @@ define( }); }); + // Finally, wire up any gestures that should be + // associated with this representation. gestureHandle = gestureService.attachGestures( element, domainObject, @@ -97,15 +114,33 @@ define( } } + // 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", refresh); } return { + // Only applicable at the element level restrict: "E", + + // Handle Angular's linking step link: link, + + // Use ng-include as a template; "inclusion" will be the real + // template path template: '', + + // Two-way bind key and parameters, get the represented domain + // object as "mct-object" scope: { key: "=", domainObject: "=mctObject", parameters: "=" } }; } diff --git a/platform/representation/src/gestures/ContextMenuGesture.js b/platform/representation/src/gestures/ContextMenuGesture.js index 0c24cf84bc..fdb8f0c001 100644 --- a/platform/representation/src/gestures/ContextMenuGesture.js +++ b/platform/representation/src/gestures/ContextMenuGesture.js @@ -16,10 +16,18 @@ define( dismissExistingMenu; /** - * Add listeners to a view such that it launches a context menu for the - * object it contains. + * Add listeners to a representation such that it launches a + * custom context menu for the domain object it contains. * * @constructor + * @param $compile Angular's $compile service + * @param $document the current document + * @param $window the active window + * @param $rootScope Angular's root scope + * @param element the jqLite-wrapped element which should exhibit + * the context mennu + * @param {DomainObject} domainObject the object on which actions + * in the context menu will be performed */ function ContextMenuGesture($compile, $document, $window, $rootScope, element, domainObject) { function showMenu(event) { @@ -73,6 +81,12 @@ define( element.on('contextmenu', showMenu); return { + /** + * Detach any event handlers associated with this gesture, + * and dismiss any visible menu. + * @method + * @memberof ContextMenuGesture + */ destroy: function () { // Scope has been destroyed, so remove all listeners. if (dismissExistingMenu) { diff --git a/platform/representation/src/gestures/DragGesture.js b/platform/representation/src/gestures/DragGesture.js index f44f7d20d2..fddfd47b7d 100644 --- a/platform/representation/src/gestures/DragGesture.js +++ b/platform/representation/src/gestures/DragGesture.js @@ -9,10 +9,16 @@ define( "use strict"; /** + * Add event handlers to a representation such that it may be + * dragged as the source for drag-drop composition. * * @constructor + * @param $log Angular's logging service + * @param element the jqLite-wrapped element which should become + * draggable + * @param {DomainObject} domainObject the domain object which + * is represented; this will be passed on drop. */ - function DragGesture($log, element, domainObject) { function startDrag(e) { var event = (e || {}).originalEvent || e; @@ -20,7 +26,10 @@ define( $log.debug("Initiating drag"); try { + // Set the data associated with the drag-drop operation event.dataTransfer.effectAllowed = 'move'; + + // Support drop as plain-text (JSON); not used internally event.dataTransfer.setData( 'text/plain', JSON.stringify({ @@ -28,12 +37,18 @@ define( model: domainObject.getModel() }) ); + + // For internal use, pass the object's identifier as + // part of the drag event.dataTransfer.setData( GestureConstants.MCT_DRAG_TYPE, domainObject.getId() ); } catch (err) { + // Exceptions at this point indicate that the browser + // do not fully support drag-and-drop (e.g. if + // dataTransfer is undefined) $log.warn([ "Could not initiate drag due to ", err.message @@ -42,11 +57,17 @@ define( } + // Mark the element as draggable, and handle the dragstart event $log.debug("Attaching drag gesture"); element.attr('draggable', 'true'); element.on('dragstart', startDrag); return { + /** + * Detach any event handlers associated with this gesture. + * @memberof DragGesture + * @method + */ destroy: function () { // Detach listener element.removeAttr('draggable'); diff --git a/platform/representation/src/gestures/DropGesture.js b/platform/representation/src/gestures/DropGesture.js index 81e9d65d9e..bcd49713fe 100644 --- a/platform/representation/src/gestures/DropGesture.js +++ b/platform/representation/src/gestures/DropGesture.js @@ -9,8 +9,14 @@ define( "use strict"; /** - * + * A DropGesture adds and maintains event handlers upon an element + * such that it may act as a drop target for drag-drop composition. + * @constructor + * @param $q Angular's $q, for promise handling + * @param element the jqLite-wrapped representation element + * @param {DomainObject} domainObject the domain object whose + * composition should be modified as a result of the drop. */ function DropGesture($q, element, domainObject) { @@ -25,6 +31,8 @@ define( // TODO: Vary this based on modifier keys event.dataTransfer.dropEffect = 'move'; + + // Indicate that we will accept the drag event.preventDefault(); // Required in Chrome? return false; } @@ -33,11 +41,15 @@ define( var event = (e || {}).originalEvent || e, id = event.dataTransfer.getData(GestureConstants.MCT_DRAG_TYPE); + // Handle the drop; add the dropped identifier to the + // destination domain object's composition, and persist + // the change. if (id) { $q.when(domainObject.useCapability( 'mutation', function (model) { var composition = model.composition; + // Don't store the same id more than once if (composition && // not-contains !(composition.map(function (i) { return i === id; @@ -48,17 +60,23 @@ define( } } )).then(function (result) { + // If mutation was successful, persist the change return result && doPersist(); }); } } - + // Listen for dragover, to indicate we'll accept a drag element.on('dragover', dragOver); + + // Listen for the drop itself element.on('drop', drop); return { + /** + * Detach any event handlers associated with this gesture. + */ destroy: function () { element.off('dragover', dragOver); element.off('drop', drop); diff --git a/platform/representation/src/gestures/GestureConstants.js b/platform/representation/src/gestures/GestureConstants.js index 05ebafc2d2..ae35fa439f 100644 --- a/platform/representation/src/gestures/GestureConstants.js +++ b/platform/representation/src/gestures/GestureConstants.js @@ -4,6 +4,15 @@ * Module defining GestureConstants. Created by vwoeltje on 11/17/14. */ define({ + /** + * The string identifier for the data type used for drag-and-drop + * composition of domain objects. (e.g. in event.dataTransfer.setData + * calls.) + */ MCT_DRAG_TYPE: 'mct-domain-object-id', + /** + * An estimate for the dimensions of a context menu, used for + * positioning. + */ MCT_MENU_DIMENSIONS: [ 170, 200 ] }); \ No newline at end of file diff --git a/platform/representation/src/gestures/GestureProvider.js b/platform/representation/src/gestures/GestureProvider.js index d77875fc67..d3686d9120 100644 --- a/platform/representation/src/gestures/GestureProvider.js +++ b/platform/representation/src/gestures/GestureProvider.js @@ -9,19 +9,35 @@ define( "use strict"; /** + * The GestureProvider exposes defined gestures. Gestures are used + * do describe and handle general-purpose interactions with the DOM + * that should be interpreted as interactions with domain objects, + * such as right-clicking to expose context menus. + * + * Gestures are defined individually as extensions of the + * `gestures` category. The gesture provider merely serves as an + * intermediary between these and the `mct-representation` directive + * where they are used. * * @constructor + * @param {Gesture[]} gestures an array of all gestures which are + * available as extensions */ function GestureProvider(gestures) { var gestureMap = {}; function releaseGesture(gesture) { + // Invoke the gesture's "destroy" method (if there is one) + // to release any held resources and detach event handlers. if (gesture && gesture.destroy) { gesture.destroy(); } } function attachGestures(element, domainObject, gestureKeys) { + // Look up the desired gestures, filter for applicability, + // and instantiate them. Maintain a reference to allow them + // to be destroyed as a group later. var attachedGestures = gestureKeys.map(function (key) { return gestureMap[key]; }).filter(function (Gesture) { @@ -34,6 +50,7 @@ define( return { destroy: function () { + // Just call all the individual "destroy" methods attachedGestures.forEach(releaseGesture); } }; @@ -46,6 +63,23 @@ define( return { + /** + * Attach a set of gestures (indicated by key) to a + * DOM element which represents a specific domain object. + * @method + * @memberof GestureProvider + * @param element the jqLite-wrapped DOM element which the + * user will interact with + * @param {DomainObject} domainObject the domain object which + * is represented by that element + * @param {string[]} gestureKeys an array of keys identifying + * which gestures should apply; these will be matched + * against the keys defined in the gestures' extension + * definitions + * @return {{ destroy: function }} an object with a `destroy` + * method which can (and should) be used when a + * gesture should no longer be applied to an element. + */ attachGestures: attachGestures }; }