Files
openmct/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js

227 lines
9.6 KiB
JavaScript

/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
define(
[],
function () {
/**
* Handles drop (from drag-and-drop) initiated changes to a swimlane.
* @constructor
*/
function TimelineSwimlaneDropHandler(swimlane) {
// Utility function; like $q.when, but synchronous (to reduce
// performance impact when wrapping synchronous values)
function asPromise(value) {
return (value && value.then) ? value : {
then: function (callback) {
return asPromise(callback(value));
}
};
}
// Check if we are in edit mode (also check parents)
function inEditMode() {
return swimlane.domainObject.hasCapability('editor') &&
swimlane.domainObject.getCapability('editor').inEditContext();
}
// Boolean and (for reduce below)
function or(a, b) {
return a || b;
}
// Check if pathA entirely contains pathB
function pathContains(swimlaneToCheck, id) {
// Check if id at a specific index matches (for map below)
function matches(pathId) {
return pathId === id;
}
// Path A contains Path B if it is longer, and all of
// B's ids match the ids in A.
return swimlaneToCheck.idPath.map(matches).reduce(or, false);
}
// Check if a swimlane contains a child with the specified id
function contains(swimlaneToCheck, id) {
// Check if a child swimlane has a matching domain object id
function matches(child) {
return child.domainObject.getId() === id;
}
// Find any one child id that matches this id
return swimlaneToCheck.children.map(matches).reduce(or, false);
}
// Initiate mutation of a domain object
function doMutate(domainObject, mutator) {
return asPromise(
domainObject.useCapability("mutation", mutator)
).then(function () {
// Persist the results of mutation
var persistence = domainObject.getCapability("persistence");
if (persistence) {
// Persist the changes
persistence.persist();
}
});
}
// Check if this swimlane is in a state where a drop-after will
// act as a drop-into-at-first position (expanded and non-empty)
function expandedForDropInto() {
return swimlane.expanded && swimlane.children.length > 0;
}
// Check if the swimlane is ready to accept a drop-into
// (instead of drop-after)
function isDropInto() {
return swimlane.highlight() || expandedForDropInto();
}
function isReorder(targetObject, droppedObject) {
var droppedContext = droppedObject.getCapability('context'),
droppedParent =
droppedContext && droppedContext.getParent(),
droppedParentId = droppedParent && droppedParent.getId();
return targetObject.getId() === droppedParentId;
}
// Choose an appropriate composition action for the drag-and-drop
function chooseAction(targetObject, droppedObject) {
var actionCapability =
targetObject.getCapability('action'),
actionKey = droppedObject.hasCapability('editor') ?
'move' : 'link';
return actionCapability && actionCapability.getActions({
key: actionKey,
selectedObject: droppedObject
})[0];
}
// Choose an index for insertion in a domain object's composition
function chooseTargetIndex(id, offset, composition) {
return Math.max(
Math.min(
(composition || []).indexOf(id) + offset,
(composition || []).length
),
0
);
}
// Insert an id into target's composition
function insert(id, target, indexOffset) {
var myId = swimlane.domainObject.getId();
return doMutate(target, function (model) {
model.composition =
model.composition.filter(function (compId) {
return compId !== id;
});
model.composition.splice(
chooseTargetIndex(myId, indexOffset, model.composition),
0,
id
);
});
}
function canDrop(targetObject, droppedObject) {
return isReorder(targetObject, droppedObject) ||
!!chooseAction(targetObject, droppedObject);
}
function drop(domainObject, targetObject, indexOffset) {
function changeIndex() {
var id = domainObject.getId();
return insert(id, targetObject, indexOffset);
}
return isReorder(targetObject, domainObject) ?
changeIndex() :
chooseAction(targetObject, domainObject)
.perform().then(changeIndex);
}
return {
/**
* Check if a drop-into should be allowed for this swimlane,
* for the provided domain object identifier.
* @param {string} id identifier for the domain object to be
* dropped
* @returns {boolean} true if this should be allowed
*/
allowDropIn: function (id, domainObject) {
return inEditMode() &&
!pathContains(swimlane, id) &&
!contains(swimlane, id) &&
canDrop(swimlane.domainObject, domainObject);
},
/**
* Check if a drop-after should be allowed for this swimlane,
* for the provided domain object identifier.
* @param {string} id identifier for the domain object to be
* dropped
* @returns {boolean} true if this should be allowed
*/
allowDropAfter: function (id, domainObject) {
var target = expandedForDropInto() ?
swimlane : swimlane.parent;
return inEditMode() &&
target &&
!pathContains(target, id) &&
canDrop(target.domainObject, domainObject);
},
/**
* Drop the provided domain object into a timeline. This is
* provided as a mandatory id, and an optional domain object
* instance; if the latter is provided, it will be removed
* from its parent before being added. (This is specifically
* to support moving Activity objects around within a Timeline.)
* @param {string} id the identifier for the domain object
* @param {DomainObject} [domainObject] the object itself
*/
drop: function (id, domainObject) {
// Get the desired drop object, and destination index
var dropInto = isDropInto(),
dropTarget = dropInto ?
swimlane.domainObject :
swimlane.parent.domainObject,
dropIndexOffset = (!dropInto) ? 1 :
(swimlane.expanded && swimlane.highlightBottom()) ?
Number.NEGATIVE_INFINITY :
Number.POSITIVE_INFINITY;
if (swimlane.highlight() || swimlane.highlightBottom()) {
return drop(domainObject, dropTarget, dropIndexOffset);
}
}
};
}
return TimelineSwimlaneDropHandler;
}
);