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
};
}