diff --git a/platform/commonUI/edit/src/EditRepresenter.js b/platform/commonUI/edit/src/EditRepresenter.js index c08832dc17..e291899474 100644 --- a/platform/commonUI/edit/src/EditRepresenter.js +++ b/platform/commonUI/edit/src/EditRepresenter.js @@ -5,25 +5,51 @@ define( function () { "use strict"; + /** + * The EditRepresenter is responsible for implementing + * representation-level behavior relevant to Edit mode. + * Specifically, this listens for changes to view configuration + * or to domain object models, and triggers persistence when + * these are detected. + * + * This is exposed as an extension of category `representers`, + * which mct-representation will utilize to add additional + * behavior to each representation. + * + * This will be called once per mct-representation directive, + * and may be reused for different domain objects and/or + * representations resulting from changes there. + * + * @constructor + */ function EditRepresenter($q, scope) { var watches = [], domainObject, key; + // Mutate and persist a new version of a domain object's model. function doPersist(model) { + // First, mutate; then, persist. return $q.when(domainObject.useCapability("mutation", function () { return model; })).then(function (result) { + // Only persist when mutation was successful return result && domainObject.getCapability("persistence").persist(); }); } + // Handle changes to model and/or view configuration function update() { + // Look up from scope; these will have been populated by + // mct-representation. var model = scope.model, configuration = scope.configuration; + // Update the configuration stored in the model, and persist. if (domainObject && domainObject.hasCapability("persistence")) { + // Configurations for specific views are stored by + // key in the "configuration" field of the model. if (key && configuration) { model.configuration = model.configuration || {}; model.configuration[key] = configuration; @@ -32,18 +58,26 @@ define( } } + // Respond to the destruction of the current representation. function destroy() { // Stop watching for changes watches.forEach(function (deregister) { deregister(); }); watches = []; } + // Handle a specific representation of a specific domain object function represent(representation, representedObject) { + // Track the key, to know which view configuration to save to. key = representation.key; + // Track the represented object domainObject = representedObject; - destroy(); // Ensure existing watches are released + // Ensure existing watches are released + destroy(); + // Watch for changes to model or configuration; keep the + // results, as $watch returns an de-registration function. + // Use the "editor" capability to check if we are in Edit mode. watches = representedObject.hasCapability("editor") ? [ scope.$watch("model", update, true), scope.$watch("configuration", update, true) @@ -51,7 +85,19 @@ define( } return { + /** + * Set the current representation in use, and the domain + * object being represented. + * + * @param {RepresentationDefinition} representation the + * definition of the representation in use + * @param {DomainObject} domainObject the domain object + * being represented + */ represent: represent, + /** + * Release any resources associated with this representer. + */ destroy: destroy }; } diff --git a/platform/commonUI/general/src/MCTDrag.js b/platform/commonUI/general/src/MCTDrag.js index b81d0031d3..fc04698798 100644 --- a/platform/commonUI/general/src/MCTDrag.js +++ b/platform/commonUI/general/src/MCTDrag.js @@ -5,29 +5,65 @@ define( function () { "use strict"; + /** + * The mct-drag directive allows drag functionality + * (in the mousedown-mousemove-mouseup sense, as opposed to + * the drag-and-drop sense) to be attached to specific + * elements. This takes the form of three attributes: + * + * * `mct-drag`: An Angular expression to evaluate during + * drag movement. + * * `mct-drag-down`: An Angular expression to evaluate + * when the drag begins. + * * `mct-drag-up`: An Angular expression to evaluate when + * dragging ends. + * + * In each case, a variable `delta` will be provided to the + * expression; this is a two-element array or the horizontal + * and vertical pixel offset of the current mouse position + * relative to the mouse position where dragging began. + * + * @constructor + * + */ function MCTDrag($document) { + // Link; install event handlers. function link(scope, element, attrs) { + // Keep a reference to the body, to attach/detach + // mouse event handlers; mousedown and mouseup cannot + // only be attached to the element being linked, as the + // mouse may leave this element during the drag. var body = $document.find('body'), initialPosition, - currentPosition, delta; + // Utility function to cause evaluation of mctDrag, + // mctDragUp, etc function fireListener(name) { + // Evaluate the expression, with current delta scope.$eval(attrs[name], { delta: delta }); // Trigger prompt digestion scope.$apply(); } + // Update positions (both actual and relative) + // based on a new mouse event object. function updatePosition(event) { - currentPosition = [ event.pageX, event.pageY ]; + // Get the current position, as an array + var currentPosition = [ event.pageX, event.pageY ]; + + // Track the initial position, if one hasn't been observed initialPosition = initialPosition || currentPosition; + + // Compute relative position delta = currentPosition.map(function (v, i) { return v - initialPosition[i]; }); } + // Called during a drag, on mousemove function continueDrag(event) { updatePosition(event); fireListener("mctDrag"); @@ -37,10 +73,14 @@ define( return false; } + // Called only when the drag ends (on mouseup) function endDrag(event) { + // Detach event handlers body.off("mouseup", endDrag); body.off("mousemove", continueDrag); + // Also call continueDrag, to fire mctDrag + // and do its usual position update continueDrag(event); fireListener("mctDragUp"); @@ -53,12 +93,18 @@ define( return false; } + // Called on mousedown on the element function startDrag(event) { + // Listen for mouse events at the body level, + // since the mouse may leave the element during + // the drag. body.on("mouseup", endDrag); body.on("mousemove", continueDrag); + // Set an initial position updatePosition(event); + // Fire listeners, including mctDrag fireListener("mctDragDown"); fireListener("mctDrag"); @@ -67,11 +113,14 @@ define( return false; } + // Listen for mousedown on the element element.on("mousedown", startDrag); } return { + // mct-drag only makes sense as an attribute restrict: "A", + // Link function, to install event handlers link: link }; } diff --git a/platform/features/layout/src/LayoutController.js b/platform/features/layout/src/LayoutController.js index 107c285632..88e8755ae1 100644 --- a/platform/features/layout/src/LayoutController.js +++ b/platform/features/layout/src/LayoutController.js @@ -5,17 +5,27 @@ define( function (LayoutDrag) { "use strict"; - var DEFAULT_DIMENSIONS = [ 12, 8 ]; + var DEFAULT_DIMENSIONS = [ 12, 8 ], + DEFAULT_GRID_SIZE = [32, 32]; + /** + * The LayoutController is responsible for supporting the + * Layout view. It arranges frames according to saved configuration + * and provides methods for updating these based on mouse + * movement. + * @constructor + * @param {Scope} $scope the controller's Angular scope + */ function LayoutController($scope) { - var width = 32, - height = 32, + var gridSize = DEFAULT_GRID_SIZE, activeDrag, activeDragId, rawPositions = {}, positions = {}; - + // Utility function to copy raw positions from configuration, + // without writing directly to configuration (to avoid triggering + // persistence from watchers during drags). function shallowCopy(obj, keys) { var copy = {}; keys.forEach(function (k) { @@ -24,15 +34,20 @@ define( return copy; } + // Convert from { positions: ..., dimensions: ... } to an + // apropriate ng-style argument, to position frames. function convertPosition(raw) { + // Multiply position/dimensions by grid size return { - left: (width * raw.position[0]) + 'px', - top: (width * raw.position[1]) + 'px', - width: (width * raw.dimensions[0]) + 'px', - height: (height * raw.dimensions[1]) + 'px' + left: (gridSize[0] * raw.position[0]) + 'px', + top: (gridSize[1] * raw.position[1]) + 'px', + width: (gridSize[0] * raw.dimensions[0]) + 'px', + height: (gridSize[1] * raw.dimensions[1]) + 'px' }; } + // Generate a default position (in its raw format) for a frame. + // Use an index to ensure that default positions are unique. function defaultPosition(index) { return { position: [index, index], @@ -40,6 +55,9 @@ define( }; } + // Store a computed position for a contained frame by its + // domain object id. Called in a forEach loop, so arguments + // are as expected there. function populatePosition(id, index) { rawPositions[id] = rawPositions[id] || defaultPosition(index || 0); @@ -47,37 +65,76 @@ define( convertPosition(rawPositions[id]); } + // Compute panel positions based on the layout's object model function lookupPanels(model) { var configuration = $scope.configuration || {}, ids = (model || {}).composition || []; - // Clear prior positions + // Pull panel positions from configuration rawPositions = shallowCopy(configuration.panels || {}, ids); + + // Clear prior computed positions positions = {}; // Update width/height that we are tracking - - // Pull values from panels field to rawPositions + gridSize = (model || {}).layoutGrid || DEFAULT_GRID_SIZE; // Compute positions and add defaults where needed ids.forEach(populatePosition); } + // Position panes when the model field changes $scope.$watch("model", lookupPanels); return { + /** + * Get a style object for a frame with the specified domain + * object identifier, suitable for use in an `ng-style` + * directive to position a frame as configured for this layout. + * @param {string} id the object identifier + * @returns {Object.} an object with + * appropriate left, width, etc fields for positioning + */ getFrameStyle: function (id) { + // Called in a loop, so just look up; the "positions" + // object is kept up to date by a watch. return positions[id]; }, + /** + * Start a drag gesture to move/resize a frame. + * + * The provided position and dimensions factors will determine + * whether this is a move or a resize, and what type it + * will be. For instance, a position factor of [1, 1] + * will move a frame along with the mouse as the drag + * proceeds, while a dimension factor of [0, 0] will leave + * dimensions unchanged. Combining these in different + * ways results in different handles; a position factor of + * [1, 0] and a dimensions factor of [-1, 0] will implement + * a left-edge resize, as the horizontal position will move + * with the mouse while the horizontal dimensions shrink in + * kind (and vertical properties remain unmodified.) + * + * @param {string} id the identifier of the domain object + * in the frame being manipulated + * @param {number[]} posFactor the position factor + * @param {number[]} dimFactor the dimensions factor + */ startDrag: function (id, posFactor, dimFactor) { activeDragId = id; activeDrag = new LayoutDrag( rawPositions[id], posFactor, dimFactor, - [ width, height ] + gridSize ); }, + /** + * Continue an active drag gesture. + * @param {number[]} delta the offset, in pixels, + * of the current pointer position, relative + * to its position when the drag started + */ continueDrag: function (delta) { if (activeDrag) { rawPositions[activeDragId] = @@ -85,13 +142,20 @@ define( populatePosition(activeDragId); } }, + /** + * End the active drag gesture. This will update the + * view configuration. + */ endDrag: function () { // Write to configuration; this is watched and // saved by the EditRepresenter. $scope.configuration = $scope.configuration || {}; + // Make sure there is a "panels" field in the + // view configuration. $scope.configuration.panels = $scope.configuration.panels || {}; + // Store the position of this panel. $scope.configuration.panels[activeDragId] = rawPositions[activeDragId]; } diff --git a/platform/features/layout/src/LayoutDrag.js b/platform/features/layout/src/LayoutDrag.js index 621956f324..5e56242115 100644 --- a/platform/features/layout/src/LayoutDrag.js +++ b/platform/features/layout/src/LayoutDrag.js @@ -5,25 +5,55 @@ define( function () { "use strict"; + /** + * Handles drag interactions on frames in layouts. This will + * provides new positions/dimensions for frames based on + * relative pixel positions provided; these will take into account + * the grid size (in a snap-to sense) and will enforce some minimums + * on both position and dimensions. + * + * The provided position and dimensions factors will determine + * whether this is a move or a resize, and what type of resize it + * will be. For instance, a position factor of [1, 1] + * will move a frame along with the mouse as the drag + * proceeds, while a dimension factor of [0, 0] will leave + * dimensions unchanged. Combining these in different + * ways results in different handles; a position factor of + * [1, 0] and a dimensions factor of [-1, 0] will implement + * a left-edge resize, as the horizontal position will move + * with the mouse while the horizontal dimensions shrink in + * kind (and vertical properties remain unmodified.) + * + * @param {object} rawPosition the initial position/dimensions + * of the frame being interacted with + * @param {number[]} posFactor the position factor + * @param {number[]} dimFactor the dimensions factor + * @param {number[]} the size of each grid element, in pixels + */ function LayoutDrag(rawPosition, posFactor, dimFactor, gridSize) { + // Convert a delta from pixel coordinates to grid coordinates, + // rounding to whole-number grid coordinates. function toGridDelta(pixelDelta) { return pixelDelta.map(function (v, i) { return Math.round(v / gridSize[i]); }); } + // Utility function to perform element-by-element multiplication function multiply(array, factors) { return array.map(function (v, i) { return v * factors[i]; }); } + // Utility function to perform element-by-element addition function add(array, other) { return array.map(function (v, i) { return v + other[i]; }); } + // Utility function to perform element-by-element max-choosing function max(array, other) { return array.map(function (v, i) { return Math.max(v, other[i]); @@ -46,6 +76,13 @@ define( } return { + /** + * Get a new position object in grid coordinates, with + * position and dimensions both offset appropriately + * according to the factors supplied in the constructor. + * @param {number[]} pixelDelta the offset from the + * original position, in pixels + */ getAdjustedPosition: getAdjustedPosition }; } diff --git a/platform/representation/src/gestures/GestureRepresenter.js b/platform/representation/src/gestures/GestureRepresenter.js index 285e9084ff..266159f3ee 100644 --- a/platform/representation/src/gestures/GestureRepresenter.js +++ b/platform/representation/src/gestures/GestureRepresenter.js @@ -5,10 +5,23 @@ define( function () { "use strict"; + /** + * The GestureRepresenter is responsible for installing predefined + * gestures upon mct-representation instances. + * Gestures themselves are pulled from the gesture service; this + * simply wraps that behavior in a Representer interface, such that + * it may be included among other such Representers used to prepare + * specific representations. + * @param {GestureService} gestureService the service which provides + * gestures + * @param {Scope} scope the Angular scope for this representation + * @param element the JQLite-wrapped mct-representation element + */ function GestureRepresenter(gestureService, scope, element) { var gestureHandle; function destroy() { + // Release any resources associated with these gestures if (gestureHandle) { gestureHandle.destroy(); } @@ -27,7 +40,19 @@ define( } return { + /** + * Set the current representation in use, and the domain + * object being represented. + * + * @param {RepresentationDefinition} representation the + * definition of the representation in use + * @param {DomainObject} domainObject the domain object + * being represented + */ represent: represent, + /** + * Release any resources associated with this representer. + */ destroy: destroy }; }