define([
|
'dojo/_base/declare',
|
'dojo/_base/lang',
|
'dojo/_base/array',
|
'dojo/aspect',
|
'dojo/dom-class',
|
'dojo/on',
|
'dojo/topic',
|
'dojo/has',
|
'dojo/when',
|
'dojo/dnd/Source',
|
'dojo/dnd/Manager',
|
'dojo/_base/NodeList',
|
'../Selection',
|
'dojo/has!touch?../util/touch'
|
], function (declare, lang, arrayUtil, aspect, domClass, on, topic, has, when, DnDSource,
|
DnDManager, NodeList, Selection, touchUtil) {
|
// Requirements
|
// * requires a store (sounds obvious, but not all Lists/Grids have stores...)
|
// * must support options.before in put calls
|
// (if undefined, put at end)
|
// * should support copy
|
// (copy should also support options.before as above)
|
|
// TODOs
|
// * consider sending items rather than nodes to onDropExternal/Internal
|
|
var GridDnDSource = declare(DnDSource, {
|
grid: null,
|
|
getObject: function (node) {
|
// summary:
|
// getObject is a method which should be defined on any source intending
|
// on interfacing with dgrid DnD.
|
|
var grid = this.grid;
|
// Extract item id from row node id (gridID-row-*).
|
return grid._trackError(function () {
|
return grid.collection.get(node.id.slice(grid.id.length + 5));
|
});
|
},
|
_legalMouseDown: function (evt) {
|
// Fix _legalMouseDown to only allow starting drag from an item
|
// (not from bodyNode outside contentNode).
|
var legal = this.inherited(arguments);
|
return legal && evt.target !== this.grid.bodyNode;
|
},
|
|
// DnD method overrides
|
onDrop: function (sourceSource, nodes, copy) {
|
var targetSource = this,
|
targetRow = this._targetAnchor = this.targetAnchor, // save for Internal
|
grid = this.grid,
|
store = grid.collection;
|
|
if (!this.before && targetRow) {
|
// target before next node if dropped within bottom half of this node
|
// (unless there's no node to target at all)
|
targetRow = targetRow.nextSibling;
|
}
|
targetRow = targetRow && grid.row(targetRow);
|
|
when(targetRow && store.get(targetRow.id), function (target) {
|
// Note: if dropping after the last row, or into an empty grid,
|
// target will be undefined. Thus, it is important for store to place
|
// item last in order if options.before is undefined.
|
|
// Delegate to onDropInternal or onDropExternal for rest of logic.
|
// These are passed the target item as an additional argument.
|
if (targetSource !== sourceSource) {
|
targetSource.onDropExternal(sourceSource, nodes, copy, target);
|
}
|
else {
|
targetSource.onDropInternal(nodes, copy, target);
|
}
|
});
|
},
|
onDropInternal: function (nodes, copy, targetItem) {
|
var grid = this.grid,
|
store = grid.collection,
|
targetSource = this,
|
anchor = targetSource._targetAnchor,
|
targetRow,
|
nodeRow;
|
|
if (anchor) { // (falsy if drop occurred in empty space after rows)
|
targetRow = this.before ? anchor.previousSibling : anchor.nextSibling;
|
}
|
|
// Don't bother continuing if the drop is really not moving anything.
|
// (Don't need to worry about edge first/last cases since dropping
|
// directly on self doesn't fire onDrop, but we do have to worry about
|
// dropping last node into empty space beyond rendered rows.)
|
nodeRow = grid.row(nodes[0]);
|
if (!copy && (targetRow === nodes[0] ||
|
(!targetItem && nodeRow && grid.down(nodeRow).element === nodes[0]))) {
|
return;
|
}
|
|
nodes.forEach(function (node) {
|
when(targetSource.getObject(node), function (object) {
|
var id = store.getIdentity(object);
|
|
// For copy DnD operations, copy object, if supported by store;
|
// otherwise settle for put anyway.
|
// (put will relocate an existing item with the same id, i.e. move).
|
grid._trackError(function () {
|
// Do no store operation if the object being moved is targetItem. This can happen when
|
// multiple, non-adjacent rows are being dragged.
|
var objectId = store.getIdentity(object);
|
var targetId = targetItem ? store.getIdentity(targetItem) : null;
|
var promise = objectId === targetId ? targetSource._getNextItem(targetItem)
|
.then(function (nextItem) {
|
targetItem = nextItem;
|
}) :
|
store[copy && store.copy ? 'copy' : 'put'](object, {
|
beforeId: targetId
|
});
|
return promise.then(function () {
|
// Self-drops won't cause the dgrid-select handler to re-fire,
|
// so update the cached node manually
|
if (targetSource._selectedNodes[id]) {
|
targetSource._selectedNodes[id] = grid.row(id).element;
|
}
|
});
|
});
|
});
|
});
|
},
|
|
_getNextItem: function (item) {
|
var grid = this.grid;
|
if (item) {
|
var row = grid.row(item);
|
if (row.element) {
|
row = grid.down(row);
|
if (row.element) {
|
return when(row.data);
|
}
|
}
|
}
|
return when(null);
|
},
|
|
onDropExternal: function (sourceSource, nodes, copy, targetItem) {
|
// Note: this default implementation expects that two grids do not
|
// share the same store. There may be more ideal implementations in the
|
// case of two grids using the same store (perhaps differentiated by
|
// query), dragging to each other.
|
var grid = this.grid,
|
store = this.grid.collection,
|
sourceGrid = sourceSource.grid;
|
|
// TODO: bail out if sourceSource.getObject isn't defined?
|
nodes.forEach(function (node, i) {
|
when(sourceSource.getObject(node), function (object) {
|
// Copy object, if supported by store; otherwise settle for put
|
// (put will relocate an existing item with the same id).
|
// Note that we use store.copy if available even for non-copy dnd:
|
// since this coming from another dnd source, always behave as if
|
// it is a new store item if possible, rather than replacing existing.
|
grid._trackError(function () {
|
return store[store.copy ? 'copy' : 'put'](object, {
|
beforeId: targetItem ? store.getIdentity(targetItem) : null
|
}).then(function () {
|
if (!copy) {
|
if (sourceGrid) {
|
// Remove original in the case of inter-grid move.
|
// (Also ensure dnd source is cleaned up properly)
|
var id = sourceGrid.collection.getIdentity(object);
|
!i && sourceSource.selectNone(); // Deselect all, one time
|
sourceSource.delItem(node.id);
|
return sourceGrid.collection.remove(id);
|
}
|
else {
|
sourceSource.deleteSelectedNodes();
|
}
|
}
|
});
|
});
|
});
|
});
|
},
|
|
onDndStart: function (source) {
|
// Listen for start events to apply style change to avatar.
|
|
this.inherited(arguments); // DnDSource.prototype.onDndStart.apply(this, arguments);
|
if (source === this) {
|
// Set avatar width to half the grid's width.
|
// Kind of a naive default, but prevents ridiculously wide avatars.
|
DnDManager.manager().avatar.node.style.width =
|
this.grid.domNode.offsetWidth / 2 + 'px';
|
}
|
},
|
|
onMouseDown: function (evt) {
|
// Cancel the drag operation on presence of more than one contact point.
|
// (This check will evaluate to false under non-touch circumstances.)
|
if (has('touch') && this.isDragging &&
|
touchUtil.countCurrentTouches(evt, this.grid.touchNode) > 1) {
|
topic.publish('/dnd/cancel');
|
DnDManager.manager().stopDrag();
|
}
|
else {
|
this.inherited(arguments);
|
}
|
},
|
|
onMouseMove: function (evt) {
|
// If we're handling touchmove, only respond to single-contact events.
|
if (!has('touch') || touchUtil.countCurrentTouches(evt, this.grid.touchNode) <= 1) {
|
this.inherited(arguments);
|
}
|
},
|
|
checkAcceptance: function (source) {
|
// Augment checkAcceptance to block drops from sources without getObject.
|
return source.getObject &&
|
DnDSource.prototype.checkAcceptance.apply(this, arguments);
|
},
|
getSelectedNodes: function () {
|
// If dgrid's Selection mixin is in use, synchronize with it, using a
|
// map of node references (updated on dgrid-[de]select events).
|
|
if (!this.grid.selection) {
|
return this.inherited(arguments);
|
}
|
var t = new NodeList(),
|
id;
|
for (id in this.grid.selection) {
|
t.push(this._selectedNodes[id]);
|
}
|
return t; // NodeList
|
}
|
// TODO: could potentially also implement copyState to jive with default
|
// onDrop* implementations (checking whether store.copy is available);
|
// not doing that just yet until we're sure about default impl.
|
});
|
|
// Mix in Selection for more resilient dnd handling, particularly when part
|
// of the selection is scrolled out of view and unrendered (which we
|
// handle below).
|
var DnD = declare(Selection, {
|
// dndSourceType: String
|
// Specifies the type which will be set for DnD items in the grid,
|
// as well as what will be accepted by it by default.
|
dndSourceType: 'dgrid-row',
|
|
// dndParams: Object
|
// Object containing params to be passed to the DnD Source constructor.
|
dndParams: null,
|
|
// dndConstructor: Function
|
// Constructor from which to instantiate the DnD Source.
|
// Defaults to the GridSource constructor defined/exposed by this module.
|
dndConstructor: GridDnDSource,
|
|
postMixInProperties: function () {
|
this.inherited(arguments);
|
// ensure dndParams is initialized
|
this.dndParams = lang.mixin({ accept: [this.dndSourceType] }, this.dndParams);
|
},
|
|
postCreate: function () {
|
this.inherited(arguments);
|
|
// Make the grid's content a DnD source/target.
|
var Source = this.dndConstructor || GridDnDSource;
|
|
var dndParams = lang.mixin(this.dndParams, {
|
// add cross-reference to grid for potential use in inter-grid drop logic
|
grid: this,
|
dropParent: this.contentNode
|
});
|
if (typeof this.expand === 'function') {
|
// If the Tree mixin is being used, allowNested needs to be set to true for DnD to work properly
|
// with the child rows. Without it, child rows will always move to the last child position.
|
dndParams.allowNested = true;
|
}
|
this.dndSource = new Source(this.bodyNode, dndParams);
|
|
// Set up select/deselect handlers to maintain references, in case selected
|
// rows are scrolled out of view and unrendered, but then dragged.
|
var selectedNodes = this.dndSource._selectedNodes = {};
|
|
function selectRow(row) {
|
selectedNodes[row.id] = row.element;
|
}
|
function deselectRow(row) {
|
delete selectedNodes[row.id];
|
// Re-sync dojo/dnd UI classes based on deselection
|
// (unfortunately there is no good programmatic hook for this)
|
domClass.remove(row.element, 'dojoDndItemSelected dojoDndItemAnchor');
|
}
|
|
this.on('dgrid-select', function (event) {
|
arrayUtil.forEach(event.rows, selectRow);
|
});
|
this.on('dgrid-deselect', function (event) {
|
arrayUtil.forEach(event.rows, deselectRow);
|
});
|
|
aspect.after(this, 'destroy', function () {
|
delete this.dndSource._selectedNodes;
|
selectedNodes = null;
|
this.dndSource.destroy();
|
}, true);
|
},
|
|
insertRow: function (object) {
|
// override to add dojoDndItem class to make the rows draggable
|
var row = this.inherited(arguments),
|
type = typeof this.getObjectDndType === 'function' ?
|
this.getObjectDndType(object) : [this.dndSourceType];
|
|
domClass.add(row, 'dojoDndItem');
|
this.dndSource.setItem(row.id, {
|
data: object,
|
type: type instanceof Array ? type : [type]
|
});
|
|
if (this.selection) {
|
var objectId = this.collection.getIdentity(object);
|
if (objectId in this.selection) {
|
this.dndSource._selectedNodes[objectId] = row;
|
}
|
}
|
|
return row;
|
},
|
|
removeRow: function (rowElement) {
|
var row = this.row(rowElement);
|
|
if (this.selection && (row.id in this.selection)) {
|
delete this.dndSource._selectedNodes[row.id];
|
}
|
|
this.dndSource.delItem(row.element.id);
|
this.inherited(arguments);
|
}
|
});
|
DnD.GridSource = GridDnDSource;
|
|
return DnD;
|
});
|