PDA

View Full Version : A DnD enabled Ext.View



Animal
20 Jul 2007, 12:49 AM
Here's my current working version with API docs:



Ext.namespace("Ext.ux");

/**
* @class Ext.ux.DDView
* A DnD enabled version of Ext.View.
* @param {Element/String} container The Element in which to create the View.
* @param {String} tpl The template string used to create the markup for each element of the View
* @param {Object} config The configuration properties. These include all the config options of
* {@link Ext.View} plus some specific to this class.<br>
* <p>
* Drag/drop is implemented by adding {@link Ext.data.Record}s to the target DDView. If copying is
* not being performed, the original {@link Ext.data.Record} is removed from the source DDView.<br>
* <p>
* The following extra CSS rules are needed to provide insertion point highlighting:<pre><code>
.x-view-drag-insert-above {
border-top:1px dotted #3366cc;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc;
}
</code></pre>
*
*/
Ext.ux.DDView = function(container, tpl, config) {
if (this.classRe.test(tpl)) {
tpl = tpl.replace(this.classRe, 'class=$1x-combo-list-item $2$1');
} else {
tpl = tpl.replace(this.tagRe, '$1 class="x-combo-list-item"');
}

Ext.ux.DDView.superclass.constructor.apply(this, arguments);
this.getEl().setStyle("outline", "0px none");
this.getEl().unselectable();
if (this.dragGroup) {
this.setDraggable(this.dragGroup.split(","));
}
if (this.dropGroup) {
this.setDroppable(this.dropGroup.split(","));
}
if (this.deletable) {
this.setDeletable();
}
this.isDirtyFlag = false;
this.addEvents({
"drop" : true
});
};

Ext.extend(Ext.ux.DDView, Ext.View, {
/** @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone. */
/** @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone. */
/** @cfg {Boolean} copy Causes drag operations to copy nodes rather than move. */
/** @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move. */

isFormField: true,

classRe: /class=(['"])(.*)\1/,

tagRe: /<(\w*).*?>/,

reset: Ext.emptyFn,

clearInvalid: Ext.form.Field.prototype.clearInvalid,

validate: function() {
return true;
},

destroy: function() {
this.purgeListeners();
this.getEl().removeAllListeners();
this.getEl().remove();
if (this.dragZone) {
if (this.dragZone.destroy) {
this.dragZone.destroy();
}
}
if (this.dropZone) {
if (this.dropZone.destroy) {
this.dropZone.destroy();
}
}
},

/** Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}. */
getName: function() {
return this.name;
},

/** Loads the View from a JSON string representing the Records to put into the Store. */
setValue: function(v) {
if (!this.store) {
throw "DDView.setValue(). DDView must be constructed with a valid Store";
}
var data = {};
data[this.store.reader.meta.root] = v ? [].concat(v) : [];
this.store.proxy = new Ext.data.MemoryProxy(data);
this.store.load();
},

/** @return {String} a parenthesised list of the ids of the Records in the View. */
getValue: function() {
var result = '(';
this.store.each(function(rec) {
result += rec.id + ',';
});
return result.substr(0, result.length - 1) + ')';
},

getIds: function() {
var i = 0, result = new Array(this.store.getCount());
this.store.each(function(rec) {
result[i++] = rec.id;
});
return result;
},

isDirty: function() {
return this.isDirtyFlag;
},

/**
* Part of the Ext.dd.DropZone interface. If no target node is found, the
* whole Element becomes the target, and this causes the drop gesture to append.
*/
getTargetFromEvent : function(e) {
var target = e.getTarget();
while ((target !== null) && (target.parentNode != this.el.dom)) {
target = target.parentNode;
}
if (!target) {
target = this.el.dom.lastChild || this.el.dom;
}
return target;
},

/**
* Create the drag data which consists of an object which has the property "ddel" as
* the drag proxy element.
*/
getDragData : function(e) {
var target = this.findItemFromChild(e.getTarget());
if(target) {
if (!this.isSelected(target)) {
delete this.ignoreNextClick;
this.onItemClick(target, this.indexOf(target), e);
this.ignoreNextClick = true;
}
var dragData = {
sourceView: this,
viewNodes: [],
records: [],
copy: this.copy || (this.allowCopy && e.ctrlKey)
};
if (this.getSelectionCount() == 1) {
var i = this.getSelectedIndexes()[0];
var n = this.getNode(i);
dragData.viewNodes.push(dragData.ddel = n);
dragData.records.push(this.store.getAt(i));
dragData.repairXY = Ext.fly(n).getXY();
} else {
dragData.ddel = Ext.fly(document.body).createChild({cls:'multi-proxy'}, null, true);
this.collectSelection(dragData);
}
return dragData;
}
return false;
},

// override the default repairXY.
getRepairXY : function(e){
return this.dragData.repairXY;
},

// Ensure the multi proxy is removed
onEndDrag: function(data, e) {
var d = Ext.get(this.dragData.ddel);
if (d && d.hasClass("multi-proxy")) {
d.remove();
}
},

/** Put the selections into the records and viewNodes Arrays. */
collectSelection: function(data) {
data.repairXY = Ext.fly(this.getSelectedNodes()[0]).getXY();
if (this.preserveSelectionOrder === true) {
Ext.each(this.getSelectedIndexes(), function(i) {
var n = this.getNode(i);
var dragNode = n.cloneNode(true);
dragNode.id = Ext.id();
data.ddel.appendChild(dragNode);
data.records.push(this.store.getAt(i));
data.viewNodes.push(n);
}, this);
} else {
var i = 0;
this.store.each(function(rec){
if (this.isSelected(i)) {
var n = this.getNode(i);
var dragNode = n.cloneNode(true);
dragNode.id = Ext.id();
data.ddel.appendChild(dragNode);
data.records.push(this.store.getAt(i));
data.viewNodes.push(n);
}
i++;
}, this);
}
},

/** Specify to which ddGroup items in this DDView may be dragged. */
setDraggable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDraggable, this);
return;
}
if (this.dragZone) {
this.dragZone.addToGroup(ddGroup);
} else {
this.dragZone = new Ext.dd.DragZone(this.getEl(), {
containerScroll: true,
ddGroup: ddGroup
});
// Draggability implies selection. DragZone's mousedown selects the element.
if (!this.multiSelect) { this.singleSelect = true; }

// Wire the DragZone's handlers up to methods in *this*
this.dragZone.getDragData = this.getDragData.createDelegate(this);
this.dragZone.getRepairXY = this.getRepairXY;
this.dragZone.onEndDrag = this.onEndDrag;
}
},

/** Specify from which ddGroup this DDView accepts drops. */
setDroppable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDroppable, this);
return;
}
if (this.dropZone) {
this.dropZone.addToGroup(ddGroup);
} else {
this.dropZone = new Ext.dd.DropZone(this.getEl(), {
owningView: this,
containerScroll: true,
ddGroup: ddGroup
});

// Wire the DropZone's handlers up to methods in *this*
this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this);
this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this);
this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this);
this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this);
this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this);
}
},

/** Decide whether to drop above or below a View node. */
getDropPoint : function(e, n, dd){
if (n == this.el.dom) { return "above"; }
var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight;
var c = t + (b - t) / 2;
var y = Ext.lib.Event.getPageY(e);
if(y <= c) {
return "above";
}else{
return "below";
}
},

isValidDropPoint: function(pt, n, data) {
if (data.viewNodes.length > 1) {
return true;
}
var d = data.viewNodes[0];
if (d == n) {
return false;
}
if ((pt == "below") && (n.nextSibling == d)) {
return false;
}
if ((pt == "above") && (n.previousSibling == d)) {
return false;
}
return true;
},

onNodeEnter : function(n, dd, e, data){
return false;
},

onNodeOver : function(n, dd, e, data){
var dragElClass = this.dropNotAllowed;
var pt = this.getDropPoint(e, n, dd);
if (this.isValidDropPoint(pt, n, data)) {
// set the insert point style on the target node
if (pt) {
var targetElClass;
if (pt == "above"){
dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";
targetElClass = "x-view-drag-insert-above";
} else {
dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";
targetElClass = "x-view-drag-insert-below";
}
if (this.lastInsertClass != targetElClass){
Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass);
this.lastInsertClass = targetElClass;
}
}
}
return dragElClass;
},

onNodeOut : function(n, dd, e, data){
this.removeDropIndicators(n);
},

onNodeDrop : function(n, dd, e, data){
if (this.fireEvent("drop", this, n, dd, e, data) === false) {
return false;
}
var pt = this.getDropPoint(e, n, dd);
var insertAt = (n == this.el.dom) ? this.nodes.length : n.nodeIndex;
if (pt == "below") {
insertAt++;
}

// Validate if dragging within a DDView
if (data.sourceView == this) {
// If the first element to be inserted below is the target node, remove it
if (pt == "below") {
if (data.viewNodes[0] == n) {
data.viewNodes.shift();
}
} else { // If the last element to be inserted above is the target node, remove it
if (data.viewNodes[data.viewNodes.length - 1] == n) {
data.viewNodes.pop();
}
}

// Nothing to drop...
if (!data.viewNodes.length) {
return false;
}

// If we are moving DOWN, then because a store.remove() takes place first,
// the insertAt must be decremented.
if (insertAt > this.store.indexOf(data.records[0])) {
insertAt--;
}
}

// Dragging from a Tree. Use the Tree's recordFromNode function.
if (data.node instanceof Ext.tree.TreeNode) {
var r = data.node.getOwnerTree().recordFromNode(data.node);
if (r) {
data.records = [ r ];
}
}

if (!data.records) {
alert("Programming problem. Drag data contained no Records");
return false;
}

for (var i = 0; i < data.records.length; i++) {
var r = data.records[i];
var dup = this.store.getById(r.id);
if (dup && (dd != this.dragZone)) {
Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1);
} else {
if (data.copy) {
this.store.insert(insertAt++, r.copy());
} else {
if (data.sourceView) {
data.sourceView.isDirtyFlag = true;
data.sourceView.store.remove(r);
}
this.store.insert(insertAt++, r);
}
this.isDirtyFlag = true;
}
}
this.dragZone.cachedTarget = null;
return true;
},

removeDropIndicators : function(n){
if(n){
Ext.fly(n).removeClass([
"x-view-drag-insert-above",
"x-view-drag-insert-below"]);
this.lastInsertClass = "_noclass";
}
},

/**
* Utility method. Add a delete option to the DDView's context menu.
* @param {String} imageUrl The URL of the "delete" icon image.
*/
setDeletable: function(imageUrl) {
if (!this.singleSelect && !this.multiSelect) {
this.singleSelect = true;
}
var c = this.getContextMenu();
this.contextMenu.on("itemclick", function(item) {
switch (item.id) {
case "delete":
this.remove(this.getSelectedIndexes());
break;
}
}, this);
this.contextMenu.add({
icon: imageUrl || AU.resolveUrl("/images/delete.gif"),
id: "delete",
text: AU.getMessage("deleteItem")
});
},

/** Return the context menu for this DDView. */
getContextMenu: function() {
if (!this.contextMenu) {
// Create the View's context menu
this.contextMenu = new Ext.menu.Menu({
id: this.id + "-contextmenu"
});
this.el.on("contextmenu", this.showContextMenu, this);
}
return this.contextMenu;
},

disableContextMenu: function() {
if (this.contextMenu) {
this.el.un("contextmenu", this.showContextMenu, this);
}
},

showContextMenu: function(e, item) {
item = this.findItemFromChild(e.getTarget());
if (item) {
e.stopEvent();
this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true);
this.contextMenu.showAt(e.getXY());
}
},

/**
* Remove {@link Ext.data.Record}s at the specified indices.
* @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove.
*/
remove: function(selectedIndices) {
selectedIndices = [].concat(selectedIndices);
for (var i = 0; i < selectedIndices.length; i++) {
var rec = this.store.getAt(selectedIndices[i]);
this.store.remove(rec);
}
},

/**
* Double click fires the event, but also, if this is draggable, and there is only one other
* related DropZone that is in another DDView, it drops the selected node on that DDView.
*/
onDblClick : function(e){
var item = this.findItemFromChild(e.getTarget());
if(item){
if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) {
return false;
}
if (this.dragGroup) {
var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true);

// Remove instances of this View's DropZone
while (targets.contains(this.dropZone)) {
targets.remove(this.dropZone);
}

// If there's only one other DropZone, and it is owned by a DDView, then drop it in
if ((targets.length == 1) && (targets[0].owningView)) {
this.dragZone.cachedTarget = null;
var el = Ext.get(targets[0].getEl());
var box = el.getBox(true);
targets[0].onNodeDrop(el.dom, {
target: el.dom,
xy: [box.x, box.y + box.height - 1]
}, null, this.getDragData(e));
}
}
}
},

onItemClick : function(item, index, e){
// The DragZone's mousedown->getDragData already handled selection
if (this.ignoreNextClick) {
delete this.ignoreNextClick;
return;
}

if(this.fireEvent("beforeclick", this, index, item, e) === false){
return false;
}
if(this.multiSelect || this.singleSelect){
if(this.multiSelect && e.shiftKey && this.lastSelection){
this.select(this.getNodes(this.indexOf(this.lastSelection), index), false);
} else if (this.isSelected(item) && e.ctrlKey) {
this.deselect(item);
}else{
this.deselect(item);
this.select(item, this.multiSelect && e.ctrlKey);
this.lastSelection = item;
}
e.preventDefault();
}
return true;
},

deselect : function(nodeInfo){
var node = this.getNode(nodeInfo);
if(node && this.isSelected(node)){
Ext.fly(node).removeClass(this.selectedClass);
this.selections.remove(node);
}
}
});


Basically, anything that is underpinned by an Ext.data.Store of Ext.data.Records can easily handle fully automatic DnD.

DigitalSkyline
21 Jul 2007, 4:28 PM
Sounds great... any chance of a live demo :)

q_no
7 Aug 2007, 1:25 AM
Hi Animal,


thanks for this DDView, it's awesome! :)

I'm only wondering if it's possible to add Ext.Form.Fields to a dragged node.
For instance: I have a list of Elements that I want to DnD (copy:true) into a large zone and on drop I want to add n Ext.form.Fields to each dropped node. Can I achieve this using a template that contains formfields or do I have to call a function onDrop that renders ext.form.elements into the according div? :)

Any ideas appreciated! :)

sjivan
7 Aug 2007, 4:21 AM
Very handy. Hope it makes the Ext core someday.

Sanjiv

franklt69
7 Aug 2007, 3:02 PM
some screenshot?


regards
Frank

Animal
9 Aug 2007, 1:08 AM
Hi Animal,


thanks for this DDView, it's awesome! :)

I'm only wondering if it's possible to add Ext.Form.Fields to a dragged node.
For instance: I have a list of Elements that I want to DnD (copy:true) into a large zone and on drop I want to add n Ext.form.Fields to each dropped node. Can I achieve this using a template that contains formfields or do I have to call a function onDrop that renders ext.form.elements into the according div? :)

Any ideas appreciated! :)

=D> I'm glad somebody could be bothered to try it out! B)

I think to get an Ext Field, you'd have to use a template that contained an "<input>" field, and transform it into an Ext.form.Field on drop.

BTW, I just updated the code in the first post to the latest version.

Photobucket uploading is down right now, but I'll post a screenshot of our first page to use this class. I know that there are going to be many more "assign X to Y" type operations in our app that will require it though.

Animal
9 Aug 2007, 3:06 AM
http://i131.photobucket.com/albums/p286/TimeTrialAnimal/DDView.jpg

What's happening here is that the "Country" component may have 3 subcomponents, "Country Details", "Area Maintanence" and "Country Sub-Entity Maintenance".

They start in the right hand DDView, titled "Available subcomponents", and may be dragged into the left hand DDView. They can be dragged the other way to deassign them.

q_no
9 Aug 2007, 7:16 AM
really nice :)

but when my dropzone gets too large I can't scroll down to the bottom while dragging... :(

Animal
9 Aug 2007, 7:40 AM
I thought the containerScroll on the DropZone handled that.

I haven't got a test case where the drop zone gets so full that it needs to scroll yet.

q_no
10 Aug 2007, 1:11 AM
Well, it works as long as the drop zone has a fixed height and overflow:auto ;)
btw: I had to override the onNodeDrop function to add some other function calls for rendering my forms into the dropped nodes. I haven't found any event-callback functionality or have I been blind? B)

Animal
10 Aug 2007, 1:31 AM
Obviously the drop zone has to have fixed height and overflow:auto to be scrollable.

As for overriding. Yes, The DDView should really offer events rather than force an override, and then call to the original implementation.

I'll have a go at that and edit post 1.

Animal
10 Aug 2007, 1:42 AM
OK, it should fire a "drop" event now.

q_no
10 Aug 2007, 4:03 AM
Thanks alot Animal :)

The drop event works fine, BUT IMO there's still one thing missing. The copy option copies each node only once, but it would be nice to have a 'unlimited' config option.
ATM I've commented out the dup-check and it works fine for me, but that might usefull for other users too. :)

Animal
10 Aug 2007, 4:21 AM
Probably best have a duplicatesAllowed config option.

q_no
13 Aug 2007, 2:27 AM
Hey Animal,

I hope I'm not hijacking your thread with my issues, but it seems like that formfields inside of DDViews aren't accessable. I have rendered some fields into dropped nodes and I can't put the cursor into the fields, never mind editing the values :(
Do you have an idea?

Animal
13 Aug 2007, 7:24 AM
Each will need to be assigned a unique id before being transformed from an ordinary HTML input field into an Ext.form.Field.

q_no
13 Aug 2007, 7:47 AM
that's not the problem. ;) The prob is, that the whole surrounding DIV is dragable and thus it's not possible to click into a formfield. When I remove the draggroup it works fine, but drag'ndrop without drag is somehow senseless.
Anyway, I tried to modify your extension to set the drag-handle to a titlebar (div inside a view tpl) but I had to give up. I have no idea how to put the draghandle on another element then on view template itself.

Animal
13 Aug 2007, 11:40 PM
OK, I think you need to set this config option in the constructor of the DragZone: http://extjs.com/deploy/ext/docs/output/Ext.dd.DragZone.html#invalidHandleTypes

q_no
14 Aug 2007, 12:20 AM
Exactly! That was the missing piece of puzzle :) thx alot!

BernardChhun
20 Aug 2007, 11:11 AM
hey Animal,

first of all, thanks for this great user extension dude \:D/

I must admit I had to hack it to make the DD ghost look like I want...anyways, I was wondering if there was a better way than overriding the getDragData function :-?

here's the code that did it:


Ext.override(Ext.ux.DDView, {
getDragData: function(e){
var target = this.findItemFromChild(e.getTarget());
if(target) {
this.handleSelection(e);
var dragData = [];
dragData.source = this;
dragData.copy = this.copy || (this.allowCopy && e.ctrlKey);
var selectedIndices = this.getSelectedIndexes();
for (var i = 0; i < selectedIndices.length; i++) {
dragData.push(this.store.getAt(selectedIndices[i]));
}

var div = document.createElement('div'); // the div element that will have the data
var dh = Ext.DomHelper;

var selNodes = this.getSelectedNodes();
if (selNodes.length == 1) {
// adding the image element without removing it from the View's container
var img = Ext.query("img", target)[0];
dh.append(div, {"tag": "img", "src": img.src, "width": img.width, "height": img.height, "title" : img.title});
} else {
div.className = 'multi-proxy';
for (var i = 0, len = selNodes.length; i < len; i++) {
var img = Ext.query("img", selNodes[i])[0];
dh.append(div, {"tag": "img", "style" : "margin-right:2px; margin-top:2px;", "src": img.src, "width": img.width / 2, "height": img.height / 2, "title" : img.title});
// a quick modelo to crop the data when it gets to 5 img elements :P
if (((i + 1) % 5) == 0 ){
dh.append(div, {"tag": "br"});
}
}
}
dragData.ddel = div;
return dragData;
}
return false;
}
});

and I've also posted some screenshots so that other users might see what is the end result.

Animal
21 Aug 2007, 2:27 AM
Kudos for pciking it up and making it run for you!

As for writing getDragData, that's how it's done. The getDragData function is a user-provided function. Ext.dd.DragSource defines it thus:



getDragData : function(e){
return this.dragData;
},


and this.dragData is defined as {}

So it's up to the author who instantiates a DragSource to provide an implementation which returns something meaningful.

Ext.ux.DDView attempts to provide a reasonable implementation, based on the selected Ext.data.Records but if you want special handling, you will need to do exactly what you did, and write your own.

Probably best not to override the version in the prototype, but rather put your implementation into the instance.

BernardChhun
21 Aug 2007, 3:59 AM
Kudos for pciking it up and making it run for you!

As for writing getDragData, that's how it's done. The getDragData function is a user-provided function. Ext.dd.DragSource defines it thus:



getDragData : function(e){
return this.dragData;
},


and this.dragData is defined as {}

So it's up to the author who instantiates a DragSource to provide an implementation which returns something meaningful.

Ext.ux.DDView attempts to provide a reasonable implementation, based on the selected Ext.data.Records but if you want special handling, you will need to do exactly what you did, and write your own.

Probably best not to override the version in the prototype, but rather put your implementation into the instance.


sweet & thanks for that quick explanation. I'll remove the Ext.override and put it in the instance.

eVizions
23 Aug 2007, 12:38 PM
Animal, absolutely wonderful piece of code. This is probably the one thing I've been looking for in Ext that isn't already included. I have been playing around with it and have bumped into an error, however. At certain points, when I drop in between two nodes in the same component, I get an error in Firebug:

data[i] has no properties
Ext.ux.DDView.js (line 251)
var dup = this.store.getById(data[i].id);

which takes place during the onNodeDrop function, although I can't pinpoint the exact issue.
Any ideas?

Animal
24 Aug 2007, 12:10 AM
I've just dropped the latest version in, you might want to try that.

getDragData now works slightly differently. It returns an object which contains an Array of Records and some extra properties, rather than just the Array with extra properties shoehorned into it.

I must say, I've never seen that error.

galdaka
24 Aug 2007, 12:58 AM
(Sorry for my bad English)

Hey Animal,

Excellent work, Is your extension registered in http://extjs.com/learn/Ext_Extensions?

Is posible to put some live examples?

Thanks in advance,

Animal
24 Aug 2007, 3:13 AM
I could cobble together an example I suppose.

Is there a good free place to put stuff like that?

Animal
24 Aug 2007, 4:11 AM
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<style>
* {
font-family:tahoma,arial,helvetica,sans-serif;
background-color:#EDF3FA;
}

.x-form-view {
cursor:default;
}

.Component {
background-color:lightblue;
border-bottom:1px groove;
cursor:pointer;
}

.asp-selected {
background-color:#000070 !important;
color:white;
}

.x-view-drag-insert-above {
border-top:1px dotted #3366cc;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc;
}

</style>
<link rel="stylesheet" type="text/css" href="../../resources/css/ext-all.css" />
<script type="text/javascript" src="../../adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="../../ext-all.js"></script>
<script type="text/javascript">
Array.prototype.contains = function(element) {
for (var i = 0; i < this.length; i++) {
if (this[i] == element) {
return true;
}
}
return false;
};
Ext.namespace("Ext.ux");

/**
* @class Ext.ux.DDView
* A DnD enabled version of Ext.View.
* @param {Element/String} container The Element in which to create the View.
* @param {String} tpl The template string used to create the markup for each element of the View
* @param {Object} config The configuration properties. These include all the config options of
* {@link Ext.View} plus some specific to this class.<br>
* <p>
* Drag/drop is implemented by adding {@link Ext.data.Record}s to the target DDView. If copying is
* not being performed, the original {@link Ext.data.Record} is removed from the source DDView.<br>
* <p>
* The following extra CSS rules are needed to provide insertion point highlighting:<pre><code>
.x-view-drag-insert-above {
border-top:1px dotted #3366cc;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc;
}
</code></pre>
*
*/
Ext.ux.DDView = function(container, tpl, config) {
Ext.ux.DDView.superclass.constructor.apply(this, arguments);
this.getEl().setStyle("outline", "0px none");
this.getEl().unselectable();
if (this.dragGroup) {
this.setDraggable(this.dragGroup.split(","));
}
if (this.dropGroup) {
this.setDroppable(this.dropGroup.split(","));
}
if (this.deletable) {
this.setDeletable();
}
this.isDirtyFlag = false;
this.addEvents({
"drop" : true
});
};

Ext.extend(Ext.ux.DDView, Ext.View, {
/** @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone. */
/** @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone. */
/** @cfg {Boolean} copy Causes drag operations to copy nodes rather than move. */
/** @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move. */

isFormField: true,

reset: Ext.emptyFn,

clearInvalid: Ext.form.Field.prototype.clearInvalid,

validate: function() {
return true;
},

destroy: function() {
this.purgeListeners();
this.getEl.removeAllListeners();
this.getEl().remove();
if (this.dragZone) {
if (this.dragZone.destroy) {
this.dragZone.destroy();
}
}
if (this.dropZone) {
if (this.dropZone.destroy) {
this.dropZone.destroy();
}
}
},

/** Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}. */
getName: function() {
return this.name;
},

/** Loads the View from a JSON string representing the Records to put into the Store. */
setValue: function(v) {
if (!this.store) {
throw "DDView.setValue(). DDView must be constructed with a valid Store";
}
var data = {};
data[this.store.reader.meta.root] = v ? [].concat(v) : [];
this.store.proxy = new Ext.data.MemoryProxy(data);
this.store.load();
},

/** @return {String} a parenthesised list of the ids of the Records in the View. */
getValue: function() {
var result = '(';
this.store.each(function(rec) {
result += rec.id + ',';
});
return result.substr(0, result.length - 1) + ')';
},

getIds: function() {
var i = 0, result = new Array(this.store.getCount());
this.store.each(function(rec) {
result[i++] = rec.id;
});
return result;
},

isDirty: function() {
return this.isDirtyFlag;
},

/**
* Part of the Ext.dd.DropZone interface. If no target node is found, the
* whole Element becomes the target, and this causes the drop gesture to append.
*/
getTargetFromEvent : function(e) {
var target = e.getTarget();
while ((target !== null) && (target.parentNode != this.el.dom)) {
target = target.parentNode;
}
if (!target) {
target = this.el.dom.lastChild || this.el.dom;
}
return target;
},

/**
* Create the drag data which consists of an object which has the property "ddel" as
* the drag proxy element.
*/
getDragData : function(e) {
var target = this.findItemFromChild(e.getTarget());
if(target) {
this.handleSelection(e);
var selNodes = this.getSelectedNodes();
var dragData = {
source: this,
copy: this.copy || (this.allowCopy && e.ctrlKey),
nodes: selNodes,
records: []
};
var selectedIndices = this.getSelectedIndexes();
for (var i = 0; i < selectedIndices.length; i++) {
dragData.records.push(this.store.getAt(selectedIndices[i]));
}
if (selNodes.length == 1) {
dragData.ddel = target.cloneNode(true); // the div element
} else {
var div = document.createElement('div'); // create the multi element drag "ghost"
div.className = 'multi-proxy';
for (var i = 0, len = selNodes.length; i < len; i++) {
div.appendChild(selNodes[i].cloneNode(true));
}
dragData.ddel = div;
}
return dragData;
}
return false;
},

/** Specify to which ddGroup items in this DDView may be dragged. */
setDraggable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDraggable, this);
return;
}
if (this.dragZone) {
this.dragZone.addToGroup(ddGroup);
} else {
this.dragZone = new Ext.dd.DragZone(this.getEl(), {
containerScroll: true,
ddGroup: ddGroup
});
// Draggability implies selection. DragZone's mousedown selects the element.
if (!this.multiSelect) { this.singleSelect = true; }

// Wire the DragZone's handlers up to methods in *this*
this.dragZone.getDragData = this.getDragData.createDelegate(this);
}
},

/** Specify from which ddGroup this DDView accepts drops. */
setDroppable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDroppable, this);
return;
}
if (this.dropZone) {
this.dropZone.addToGroup(ddGroup);
} else {
this.dropZone = new Ext.dd.DropZone(this.getEl(), {
containerScroll: true,
ddGroup: ddGroup
});

// Wire the DropZone's handlers up to methods in *this*
this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this);
this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this);
this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this);
this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this);
this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this);
}
},

/** Decide whether to drop above or below a View node. */
getDropPoint : function(e, n, dd){
if (n == this.el.dom) { return "above"; }
var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight;
var c = t + (b - t) / 2;
var y = Ext.lib.Event.getPageY(e);
if(y <= c) {
return "above";
}else{
return "below";
}
},

onNodeEnter : function(n, dd, e, data){
return false;
},

onNodeOver : function(n, dd, e, data){
var pt = this.getDropPoint(e, n, dd);
// set the insert point style on the target node
var dragElClass = this.dropNotAllowed;
if (pt) {
var targetElClass;
if (pt == "above"){
dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";
targetElClass = "x-view-drag-insert-above";
} else {
dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";
targetElClass = "x-view-drag-insert-below";
}
if (this.lastInsertClass != targetElClass){
Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass);
this.lastInsertClass = targetElClass;
}
}
return dragElClass;
},

onNodeOut : function(n, dd, e, data){
this.removeDropIndicators(n);
},

onNodeDrop : function(n, dd, e, data){
if (this.fireEvent("drop", this, n, dd, e, data) === false) {
return false;
}
var pt = this.getDropPoint(e, n, dd);
var insertAt = (n == this.el.dom) ? this.nodes.length : n.nodeIndex;
if (pt == "below") { insertAt++; }
for (var i = 0; i < data.records.length; i++) {
var r = data.records[i];
var dup = this.store.getById(r.id);
if (dup && (dd != this.dragZone)) {
Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1);
} else {
if (data.copy) {
this.store.insert(insertAt++, r.copy());
} else {
data.source.isDirtyFlag = true;
r.store.remove(r);
this.store.insert(insertAt++, r);
}
this.isDirtyFlag = true;
}
}
this.dragZone.cachedTarget = null;
return true;
},

removeDropIndicators : function(n){
if(n){
Ext.fly(n).removeClass([
"x-view-drag-insert-above",
"x-view-drag-insert-below"]);
this.lastInsertClass = "_noclass";
}
},

/**
* Utility method. Add a delete option to the DDView's context menu.
* @param {String} imageUrl The URL of the "delete" icon image.
*/
setDeletable: function(imageUrl) {
if (!this.singleSelect && !this.multiSelect) {
this.singleSelect = true;
}
var c = this.getContextMenu();
this.contextMenu.on("itemclick", function(item) {
switch (item.id) {
case "delete":
this.remove(this.getSelectedIndexes());
break;
}
}, this);
this.contextMenu.add({
icon: imageUrl || AU.resolveUrl("/images/delete.gif"),
id: "delete",
text: AU.getMessage("deleteItem")
});
},

/** Return the context menu for this DDView. */
getContextMenu: function() {
if (!this.contextMenu) {
// Create the View's context menu
this.contextMenu = new Ext.menu.Menu({
id: this.id + "-contextmenu"
});
this.el.on("contextmenu", this.showContextMenu, this);
}
return this.contextMenu;
},

disableContextMenu: function() {
if (this.contextMenu) {
this.el.un("contextmenu", this.showContextMenu, this);
}
},

showContextMenu: function(e, item) {
item = this.findItemFromChild(e.getTarget());
if (item) {
e.stopEvent();
this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true);
this.contextMenu.showAt(e.getXY());
}
},

/**
* Remove {@link Ext.data.Record}s at the specified indices.
* @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove.
*/
remove: function(selectedIndices) {
selectedIndices = [].concat(selectedIndices);
for (var i = 0; i < selectedIndices.length; i++) {
var rec = this.store.getAt(selectedIndices[i]);
this.store.remove(rec);
}
},

/**
* Double click fires the event, but also, if this is draggable, and there is only one other
* related DropZone, it transfers the selected node.
*/
onDblClick : function(e){
var item = this.findItemFromChild(e.getTarget());
if(item){
if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) {
return false;
}
if (this.dragGroup) {
var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true);
while (targets.contains(this.dropZone)) {
targets.remove(this.dropZone);
}
if (targets.length == 1) {
this.dragZone.cachedTarget = null;
var el = Ext.get(targets[0].getEl());
var box = el.getBox(true);
targets[0].onNodeDrop(el.dom, {
target: el.dom,
xy: [box.x, box.y + box.height - 1]
}, null, this.getDragData(e));
}
}
}
},

handleSelection: function(e) {
this.dragZone.cachedTarget = null;
var item = this.findItemFromChild(e.getTarget());
if (!item) {
this.clearSelections(true);
return;
}
if (item && (this.multiSelect || this.singleSelect)){
if(this.multiSelect && e.shiftKey && (!e.ctrlKey) && this.lastSelection){
this.select(this.getNodes(this.indexOf(this.lastSelection), item.nodeIndex), false);
}else if (this.isSelected(this.getNode(item)) && e.ctrlKey){
this.unselect(item);
} else {
this.select(item, this.multiSelect && e.ctrlKey);
this.lastSelection = item;
}
}
},

onItemClick : function(item, index, e){
if(this.fireEvent("beforeclick", this, index, item, e) === false){
return false;
}
return true;
},

unselect : function(nodeInfo, suppressEvent){
var node = this.getNode(nodeInfo);
if(node && this.isSelected(node)){
if(this.fireEvent("beforeselect", this, node, this.selections) !== false){
Ext.fly(node).removeClass(this.selectedClass);
this.selections.remove(node);
if(!suppressEvent){
this.fireEvent("selectionchange", this, this.selections);
}
}
}
}
});

function initializePage() {
var collection=[
{'id':'40','entityImageUrl':'../shared/icons/fam/user_add.png','componentDescription':'Add User'},
{'id':'27','entityImageUrl':'../shared/icons/fam/user_delete.png','componentDescription':'Delete User'},
{'id':'28','entityImageUrl':'../shared/icons/fam/user_comment.png','componentDescription':'Comment on User'}
];
var rec=Ext.data.Record.create([
{name:'id'},
{name:'entityImageUrl'},
{name:'componentDescription'}
]);
var reader=new Ext.data.JsonReader({
root:'collection',
id:'id'
},rec);
var ds=new Ext.data.Store({
proxy:new Ext.data.MemoryProxy({collection:collection}),
reader:reader
});
var view=new Ext.ux.DDView('left-view-container','<div id=\u0027subcomponent_{id}\u0027 class=\u0027Subcomponent\u0027><img align=\u0027top\u0027 height=\u002716px\u0027 width=\u002716px\u0027 src=\u0027{entityImageUrl}\u0027>{componentDescription}</div>',{
isFormField:true,
name:'subComponents',
dragGroup:'availComponentDDGroup,subComponentDDGroup',
dropGroup:'availComponentDDGroup,subComponentDDGroup',
selectedClass: 'asp-selected',
jsonRoot: 'collection',
store: ds
});
ds.load();

collection=[];
rec=Ext.data.Record.create([
{name:'id'},
{name:'entityImageUrl'},
{name:'componentDescription'}
]);
reader=new Ext.data.JsonReader({
root:'collection',
id:'id'
},rec);
ds=new Ext.data.Store({
proxy:new Ext.data.MemoryProxy({collection:collection}),
reader:reader
});
view=new Ext.ux.DDView('right-view-container','<div id=\u0027component_{id}\u0027 class=\u0027Component\u0027><img align=\u0027top\u0027 height=\u002716px\u0027 width=\u002716px\u0027 src=\u0027{entityImageUrl}\u0027>{componentDescription}</div>',{
isFormField:true,
name:'availableSubComponents',
multiSelect: true,
dragGroup:'subComponentDDGroup',
dropGroup:'availComponentDDGroup',
selectedClass: 'asp-selected',
jsonRoot: 'collection',
store: ds
});
ds.load();
}
Ext.onReady(initializePage);
</script>
</head>
<body>
<form class="x-form" id="the-form">
<div class="x-form-ct x-form-column x-form-label-left">
<fieldset class="x-form-fieldset x-form-label-left" style="margin-left:10px" >
<legend>Subcomponents</legend>
<div id="left-view-container" class="x-form-view" style="overflow:auto;height:215px">
</div>
</fieldset>
</div>
<div class="x-form-ct x-form-column x-form-label-left ">
<fieldset class="x-form-fieldset x-form-label-left" style="margin-left:10px">
<legend>Available subcomponents</legend>
<div id="right-view-container" class="x-form-view" style="overflow:auto;height:215px">
</div>
</fieldset>
</div>
</form>
</body>
</html>

galdaka
24 Aug 2007, 5:40 AM
Excellent example!!

Thanks Animal!!

I not have host for allocate your examples, sorry. If anybody has it please leave it!!!!!

Coud you please add it to the user community extension page ? => http://extjs.com/learn/Ext_Extensions I think that is good for maintain the versions.

Thanks in advance!

eVizions
24 Aug 2007, 7:42 AM
Animal, have you taken a stab at making this work with Ext2.0? If not, I'll give it a try.

Animal
24 Aug 2007, 7:51 AM
I have not, but I'd like to see it done! I'll upgrade when it's released.

Just one hint. Jack suggested that it should extend DataView, as View is deprecated in 2.0.

Animal
24 Aug 2007, 7:53 AM
I still think there's a problem with the new DataView in that it selects on click and not mousedown. This is not how DnD works if you try dragging a file in Windoze Explorer.

The mousedown selects, and then you are dragging a selected element.

My DDView overrides the View's selection code, but I think the Ext 2.0 DataView should do it right!

eVizions
24 Aug 2007, 8:41 AM
Hmm... okay, I'll take a look and see what I can come up with. I've also put up your example on my server so people can take a look... hope you don't mind.

Animal's Ext.ux.DDView (http://evizions.com/Ext.ux.DDView)

eVizions
24 Aug 2007, 2:56 PM
I've got a "working" model for Ext2.0... I also added in another parameter, which I called 'dragLayout' (for lack of a better name). For your example, Animal, it would default to 'list', getDropPoint looks at top/bottom, whereas if it's set to 'block', it looks for left/right (although it still returns top/bottom) so you can have inline or floated objects. It still isn't perfect, but at least it's a good start. Awesome extension, Animal. I'll see if I can tweak it over the weekend.


Ext.namespace("Ext.ux");

/**
* @class Ext.ux.DDView
* A DnD enabled version of Ext.View.
* @param {Object} config The configuration properties. These include all the config options of
* {@link Ext.View} plus some specific to this class.<br>
* <p>
* Drag/drop is implemented by adding {@link Ext.data.Record}s to the target DDView. If copying is
* not being performed, the original {@link Ext.data.Record} is removed from the source DDView.<br>
* <p>
* The following extra CSS rules are needed to provide insertion point highlighting:<pre><code>
.x-view-drag-insert-above {
border-top:1px dotted #3366cc;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc;
}
</code></pre>
*
*/

Ext.ux.DDView = function(config) {
Ext.ux.DDView.superclass.constructor.apply(this, arguments);

this.isDirtyFlag = false;
this.addEvents({
"drop" : true
});
};

Ext.extend(Ext.ux.DDView, Ext.DataView, {
/** @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone. */
/** @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone. */
/** @cfg {Boolean} copy Causes drag operations to copy nodes rather than move. */
/** @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move. */

isFormField: true,

reset: Ext.emptyFn,

dragLayout: 'list', // list or block

clearInvalid: Ext.form.Field.prototype.clearInvalid,

onRender : function(){
if(!this.el){
this.el = document.createElement('div');
//this.el = this.ownerCt.el.createChild('div');
}
Ext.ux.DDView.superclass.onRender.apply(this, arguments);
},

afterRender : function(){
this.getEl().autoHeight();
Ext.ux.DDView.superclass.afterRender.apply(this, arguments);
this.getEl().setStyle("outline", "0px none");
this.getEl().unselectable();
if (this.dragGroup) {
this.setDraggable(this.dragGroup.split(","));
}
if (this.dropGroup) {
this.setDroppable(this.dropGroup.split(","));
}
if (this.deletable) {
this.setDeletable();
}
},

validate: function() {
return true;
},

destroy: function() {
this.purgeListeners();
this.getEl().removeAllListeners();
this.getEl().remove();
if (this.dragZone) {
if (this.dragZone.destroy) {
this.dragZone.destroy();
}
}
if (this.dropZone) {
if (this.dropZone.destroy) {
this.dropZone.destroy();
}
}
},

/** Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}. */
getName: function() {
return this.name;
},

/** Loads the View from a JSON string representing the Records to put into the Store. */
setValue: function(v) {
if (!this.store) {
throw "DDView.setValue(). DDView must be constructed with a valid Store";
}
var data = {};
data[this.store.reader.meta.root] = v ? [].concat(v) : [];
this.store.proxy = new Ext.data.MemoryProxy(data);
this.store.load();
},

/** @return {String} a parenthesised list of the ids of the Records in the View. */
getValue: function() {
var result = '(';
this.store.each(function(rec) {
result += rec.id + ',';
});
return result.substr(0, result.length - 1) + ')';
},

getIds: function() {
var i = 0, result = new Array(this.store.getCount());
this.store.each(function(rec) {
result[i++] = rec.id;
});
return result;
},

isDirty: function() {
return this.isDirtyFlag;
},

/**
* Part of the Ext.dd.DropZone interface. If no target node is found, the
* whole Element becomes the target, and this causes the drop gesture to append.
*/
getTargetFromEvent : function(e) {
var target = e.getTarget();
while ((target !== null) && (target.parentNode != this.el.dom)) {
target = target.parentNode;
}
if (!target) {
target = this.el.dom.lastChild || this.el.dom;
}
return target;
},

/**
* Create the drag data which consists of an object which has the property "ddel" as
* the drag proxy element.
*/
getDragData : function(e) {
var target = this.findItemFromChild(e.getTarget());
if(target) {
this.handleSelection(e);
var selNodes = this.getSelectedNodes();
var dragData = {
source: this,
copy: this.copy || (this.allowCopy && e.ctrlKey),
nodes: selNodes,
records: []
};

var selectedIndices = this.getSelectedIndexes();
for (var i = 0; i < selectedIndices.length; i++) {
dragData.records.push(this.store.getAt(selectedIndices[i]));
}
if (selNodes.length == 1) {
dragData.ddel = target.cloneNode(true); // the div element
} else {
var div = document.createElement('div'); // create the multi element drag "ghost"
div.className = 'multi-proxy';
for (var i = 0, len = selNodes.length; i < len; i++) {
div.appendChild(selNodes[i].cloneNode(true));
}
dragData.ddel = div;
}
return dragData;
}
return false;
},

/** Specify to which ddGroup items in this DDView may be dragged. */
setDraggable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDraggable, this);
return;
}
if (this.dragZone) {
this.dragZone.addToGroup(ddGroup);
} else {
this.dragZone = new Ext.dd.DragZone(this.getEl(), {
containerScroll: true,
ddGroup: ddGroup
});
// Draggability implies selection. DragZone's mousedown selects the element.
if (!this.multiSelect) { this.singleSelect = true; }

// Wire the DragZone's handlers up to methods in *this*
this.dragZone.getDragData = this.getDragData.createDelegate(this);
}
},

/** Specify from which ddGroup this DDView accepts drops. */
setDroppable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDroppable, this);
return;
}
if (this.dropZone) {
this.dropZone.addToGroup(ddGroup);
} else {
this.dropZone = new Ext.dd.DropZone(this.getEl(), {
containerScroll: true,
ddGroup: ddGroup
});

// Wire the DropZone's handlers up to methods in *this*
this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this);
this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this);
this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this);
this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this);
this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this);
}
},

/** Decide whether to drop above or below a View node. */
getDropPoint : function(e, n, dd){
if(this.dragDisplay == 'block'){
if (n == this.el.dom) { return "below"; }
var l = Ext.lib.Dom.getX(n), r = l + n.offsetWidth;
var c = l + (r - l) / 2;
var x = Ext.lib.Event.getPageX(e);
if(x <= c) {
return 'above';
}else{
return 'below';
}
}else{
if (n == this.el.dom) { return "above"; }
var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight;
var c = t + (b - t) / 2;
var y = Ext.lib.Event.getPageY(e);
if(y <= c) {
return "above";
}else{
return "below";
}
}
},

onNodeEnter : function(n, dd, e, data){
return false;
},

onNodeOver : function(n, dd, e, data){
var pt = this.getDropPoint(e, n, dd);
// set the insert point style on the target node
var dragElClass = this.dropNotAllowed;
if (pt) {
var targetElClass;
if (pt == "above"){
dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";
targetElClass = "x-view-drag-insert-above";
} else {
dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";
targetElClass = "x-view-drag-insert-below";
}
if (this.lastInsertClass != targetElClass){
Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass);
this.lastInsertClass = targetElClass;
}
}
return dragElClass;
},

onNodeOut : function(n, dd, e, data){
this.removeDropIndicators(n);
},

onNodeDrop : function(n, dd, e, data){
if (this.fireEvent("drop", this, n, dd, e, data) === false) {
return false;
}
var pt = this.getDropPoint(e, n, dd);
var tIndex = (e.target.parentNode == this.el.dom) ? e.target.viewIndex : e.target.parentNode.viewIndex;
var insertAt = (n == this.el.dom) ? (this.store.getTotalCount()-1) : (tIndex);
if (pt == "below") { insertAt++; }
for (var i = 0; i < data.records.length; i++) {
var r = data.records[i];
var dup = this.store.getById(r.id);
if (dup && (dd != this.dragZone)) {
Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1);
} else {
if (data.copy) {
this.store.insert(insertAt, r.copy());
} else {
if(this.store.indexOfId(r.id) < insertAt) insertAt--;
data.source.isDirtyFlag = true;
this.store.remove(r);
this.store.insert(insertAt, r);
}
this.isDirtyFlag = true;
}
}
this.refresh();
this.dragZone.cachedTarget = null;
return true;
},

removeDropIndicators : function(n){
if(n){
Ext.fly(n).removeClass([
"x-view-drag-insert-above",
"x-view-drag-insert-below"]);
this.lastInsertClass = "_noclass";
}
},

/**
* Utility method. Add a delete option to the DDView's context menu.
* @param {String} imageUrl The URL of the "delete" icon image.
*/
setDeletable: function(imageUrl) {
if (!this.singleSelect && !this.multiSelect) {
this.singleSelect = true;
}
var c = this.getContextMenu();
this.contextMenu.on("itemclick", function(item) {
switch (item.id) {
case "delete":
this.remove(this.getSelectedIndexes());
break;
}
}, this);
this.contextMenu.add({
icon: imageUrl || AU.resolveUrl("/images/delete.gif"),
id: "delete",
text: AU.getMessage("deleteItem")
});
},

/** Return the context menu for this DDView. */
getContextMenu: function() {
if (!this.contextMenu) {
// Create the View's context menu
this.contextMenu = new Ext.menu.Menu({
id: this.id + "-contextmenu"
});
this.el.on("contextmenu", this.showContextMenu, this);
}
return this.contextMenu;
},

disableContextMenu: function() {
if (this.contextMenu) {
this.el.un("contextmenu", this.showContextMenu, this);
}
},

showContextMenu: function(e, item) {
item = this.findItemFromChild(e.getTarget());
if (item) {
e.stopEvent();
this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true);
this.contextMenu.showAt(e.getXY());
}
},

/**
* Remove {@link Ext.data.Record}s at the specified indices.
* @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove.
*/
remove: function(selectedIndices) {
selectedIndices = [].concat(selectedIndices);
for (var i = 0; i < selectedIndices.length; i++) {
var rec = this.store.getAt(selectedIndices[i]);
this.store.remove(rec);
}
},

/**
* Double click fires the event, but also, if this is draggable, and there is only one other
* related DropZone, it transfers the selected node.
*/
onDblClick : function(e){
var item = this.findItemFromChild(e.getTarget());
if(item){
if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) {
return false;
}
if (this.dragGroup) {
var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true);
while (targets.contains(this.dropZone)) {
targets.remove(this.dropZone);
}
if (targets.length == 1) {
this.dragZone.cachedTarget = null;
var el = Ext.get(targets[0].getEl());
var box = el.getBox(true);
targets[0].onNodeDrop(el.dom, {
target: el.dom,
xy: [box.x, box.y + box.height - 1]
}, null, this.getDragData(e));
}
}
}
},

handleSelection: function(e) {
this.dragZone.cachedTarget = null;
var item = this.findItemFromChild(e.getTarget());
if (!item) {
this.clearSelections(true);
return;
}
if (item && (this.multiSelect || this.singleSelect)){
if(this.multiSelect && e.shiftKey && (!e.ctrlKey) && this.lastSelection){
this.select(this.getNodes(this.indexOf(this.lastSelection), item.nodeIndex), false);
}else if (this.isSelected(this.getNode(item)) && e.ctrlKey){
this.unselect(item);
} else {
this.select(item, this.multiSelect && e.ctrlKey);
this.lastSelection = item;
}
}
},

onItemClick : function(item, index, e){
if(this.fireEvent("beforeclick", this, index, item, e) === false){
return false;
}
return true;
},

unselect : function(nodeInfo, suppressEvent){
var node = this.getNode(nodeInfo);
if(node && this.isSelected(node)){
if(this.fireEvent("beforeselect", this, node, this.selections) !== false){
Ext.fly(node).removeClass(this.selectedClass);
this.selections.remove(node);
if(!suppressEvent){
this.fireEvent("selectionchange", this, this.selections);
}
}
}
}
});

Animal
24 Aug 2007, 11:13 PM
Looks good. I don't think you should really have



this.getEl().setStyle("outline", "0px none");
this.getEl().setStyle('border', '1px solid blue');


It should be entirely up to the user how to style the element. He should use a class on the container, and a CSS rule.

Animal
24 Aug 2007, 11:15 PM
I'm getting some 404s on some images in your live demo which causes some strange display characteristics.

The drag proxy is huge for a while, then settles back into being just a line of text.

Then, when dropped, the dropped node is again huge for a while before settling back to be just one line.

BernardChhun
27 Aug 2007, 6:26 AM
oyo Animal,

I just stumbled into this small typo in the destroy function:

before:


destroy: function() {
this.purgeListeners();
this.getEl.removeAllListeners();
this.getEl().remove();

if (this.dragZone) {
if (this.dragZone.destroy) {
this.dragZone.destroy();
}
}
if (this.dropZone) {
if (this.dropZone.destroy) {
this.dropZone.destroy();
}
}
}

after:


destroy: function() {
this.purgeListeners();
this.getEl().removeAllListeners();
this.getEl().remove();
if (this.dragZone) {
if (this.dragZone.destroy) {
this.dragZone.destroy();
}
}
if (this.dropZone) {
if (this.dropZone.destroy) {
this.dropZone.destroy();
}
}
}

eVizions
27 Aug 2007, 7:37 AM
Looks good. I don't think you should really have



this.getEl().setStyle("outline", "0px none");
this.getEl().setStyle('border', '1px solid blue');


It should be entirely up to the user how to style the element. He should use a class on the container, and a CSS rule.

Oops. I added the border just so I could see how it was laying out when I laid everything out as float:left. Guess I forgot to remove it. The outline, though, was from your version and I think you had it in there for a reason (when it is used with an input element, I think). But you're right - styling should be left to CSS.

Animal
28 Aug 2007, 1:20 AM
OK, I edited post #1. Drag of multiple selects retains the original order.

destroy is fixed, thanks Bernard.

Also when dragging DOWN the same View, it got out of step because the remove() which took place first moved items below up by 1, so the insertAt point was wrong. This has been fixed.

Troy Wolf
28 Aug 2007, 10:48 AM
Thanks to eVizions for hosting Animal's example. (As Animal pointed out, eVizions' example has some strange missing image box that appears when dragging, but otherwise works. I don't have this issue when I copy Animal's example to my own server.)

Animal, both at eVizion's hosted example (http://evizions.com/Ext.ux.DDView), and when I copied your example to my own server, I see an issue.

Simply drag the top item in "Subcomponents" to the bottom of the same list. (Don't drag it over to the "Available subcomponents" container.) Do this twice to see the problem. Do it more times to see the problem continue.

To explain the problem, when the example loads, the Subcomponents list is:

Add User
Delete User
Comment on User

If you drag "Add User" to the bottom of the list, then drag "Delete User" to the bottom of the list, you'll end up with:

Comment on User
Add User
Add User

I'm particularly interested in a fix or explanation as the left side of this example is just about exactly the functionality I need for my app.

Thanks!

Animal
29 Aug 2007, 12:08 AM
I noticed that a couple of days ago too... a bit embarassingly crap really!

It's the problem of dragging down in the same View which I mentioned above. The latest code should fix that.

The display problems at eVizions are because the icon image URLs are wrong, and don't exist, and the eVizions server responds with a full HTML document explaining that the file is not found (as you would expect on a public facing web server).

Troy Wolf
29 Aug 2007, 4:51 AM
It's the problem of dragging down in the same View which I mentioned above. The latest code should fix that.
I see...you updated the code in post #1. I grabbed your example code from your page 3 post yesterday at about 1PM CST, and it produced the issue. Perhaps apply the fix to that example as well.

Animal, I very much appreciate you sharing your expertise with the Ext code and concepts and even writing complete example apps--more than anything else, that helps me wrap my brain around the concepts. THANKS!

Animal
29 Aug 2007, 5:09 AM
It's fixed here.

I'm contiuing to update it.

I'm thinking there should be a universal DnD strategy between Views, Grids and Trees.

I think it's something Jack should look into.

Basically, drag data should be Records. Ext.data.Records.

This just means that the Tree will have to implement a method to create a Node from a Record, and a method to extract a Record from a Node.

I'll drop my latest in soon when I've done some testing. I can now DnD back and forth between a Tree and a DDView.

The DDView checks whether the drag data contains an instance of Ext.tree.TreeNode, and calls the owner Tree's recordFromNode() method on it to get a Record, and adds that to its Store.

I can't repro the dragging down problem with my current code. AFAIK, eVizions has not updated his to use the latest.

Animal
30 Aug 2007, 1:24 AM
OK, latest drop in post #1.

Will accept drops from an Ext.tree.TreePanel if you add the function recordFromNode to that tree. So in my new page, I have a Tree on the left, and a DDView on the right. I have



myTree.recordFromNode = function(n) {
var result = new myDDView.store.recordType({
id: n.attributes.userId,
entityImageUrl: n.attributes.icon,
knownAs: n.attributes.text
}, n.attributes.userId);
n.parentNode.removeChild(n);
return result;
};


So that I can deallocate a User, and his Record is correctly put back into the View.

What's needed is to be able to drive a Tree using Records, so that each Node contains a Record, and there's some kind of mapping from the Record's Fields to node properties like text, icon, qtip, id etc.

All DnD could then be simplified.

Troy Wolf
30 Aug 2007, 6:57 AM
It's fixed here....I can't repro the dragging down problem with my current code.
At 9:55 AM CST 8/30/2007, I copied and pasted the code from the post in the URL below and I still get the drag down problem.
http://extjs.com/forum/showthread.php?p=57292#post57292

jack.slocum
30 Aug 2007, 7:00 AM
I still think there's a problem with the new DataView in that it selects on click and not mousedown. This is not how DnD works if you try dragging a file in Windoze Explorer.

The mousedown selects, and then you are dragging a selected element.

My DDView overrides the View's selection code, but I think the Ext 2.0 DataView should do it right!

Selection should happen on click, which allows other processes to do what they need to without interference or event handler order (e.g. DragDrop, DragSelector). Take a look at the newly updated 2.0 Image Organizer example which has both DragDrop and the DragSelector plugin applied. It's pretty easy to pass the mousedown to the view (as a click) if the user has clicked on an unselected item. Here's an excerpt:


getDragData : function(e){
var target = e.getTarget('.thumb-wrap');
if(target){
var view = this.view;
if(!view.isSelected(target)){
view.onClick(e);
}
var selNodes = view.getSelectedNodes();

Speaking of DragSelector, have you tried DDView with it?

Animal
30 Aug 2007, 7:09 AM
At 9:55 AM CST 8/30/2007, I copied and pasted the code from the post in the URL below and I still get the drag down problem.
http://extjs.com/forum/showthread.php?p=57292#post57292

http://extjs.com/forum/showthread.php?p=47875#post47875 is what I update.

Animal
30 Aug 2007, 7:10 AM
Selection should happen on click, which allows other processes to do what they need to without interference or event handler order (e.g. DragDrop, DragSelector). Take a look at the newly updated 2.0 Image Organizer example which has both DragDrop and the DragSelector plugin applied. It's pretty easy to pass the mousedown to the view (as a click) if the user has clicked on an unselected item. Here's an excerpt:


getDragData : function(e){
var target = e.getTarget('.thumb-wrap');
if(target){
var view = this.view;
if(!view.isSelected(target)){
view.onClick(e);
}
var selNodes = view.getSelectedNodes();

Speaking of DragSelector, have you tried DDView with it?

I'll take a look at removing my mousedown selection code. It would make the class simpler.

Troy Wolf
30 Aug 2007, 7:38 AM
http://extjs.com/forum/showthread.php?p=47875#post47875 is what I update.
Gotcha, so copy the fully working demo script from
http://extjs.com/forum/showthread.php?p=57292#post57292
Then replace the DDView class with the most recent from
http://extjs.com/forum/showthread.php?p=47875#post47875

I did this, and of course you are right--it works beautiful. Sorry for my confusion.

Troy Wolf
30 Aug 2007, 10:08 AM
In the code below, I am confused by the dragGroup and dropGroup assignments. The values appear to be element ids that are valid drag or drop targets, but I can't find anything in the code that actually has an id or name of 'availComponentDDGroup' or 'subComponentDDGroup'. What is going on here?


var view=new Ext.ux.DDView('left-view-container','<div id=\u0027subcomponent_{id}\u0027 class=\u0027Subcomponent\u0027><img align=\u0027top\u0027 height=\u002716px\u0027 width=\u002716px\u0027 src=\u0027{entityImageUrl}\u0027>{componentDescription}</div>',{
isFormField:true,
name:'subComponents',
dragGroup:'availComponentDDGroup,subComponentDDGroup',
dropGroup:'availComponentDDGroup,subComponentDDGroup',
selectedClass: 'asp-selected',
jsonRoot: 'collection',
store: ds
});

Let me say again, what has helped me most is the fully working HTML/CSS/JS example you posted on page three. I guess I learn "backwards" by working from the result backwards to see the methods used to produce that result. (This is probably why Geometry was my favorite math class--I loved proving theorems.) Through this working example, in addition to your DDView object, Ext data stores and readers are really starting to make sense to me.

Troy Wolf
30 Aug 2007, 10:26 AM
Yes, Troy needs more hand holding...

When you create a new DDView, you set isFormField:true. I assume this is for several purposes, but would one purpose be to allow you to pass the data back via a form submit?

If so, in the example from page 3 of this thread, the HTML has a form inline and the DDView is created in an element within that form. Does the DDView become a "field" of that form? If so, does the code walk up the nodes to find the nearest parent form?

Rather than an inline form in my HTML, I'm creating a Ext form using
var frmFoo = new Ext.form.Form({... How would I create my DDView to be part of this form?

Or, as I suspect, have I completely missed the boat here? ~o)

Animal
31 Aug 2007, 12:26 AM
The dragGroup, dropGroup names are to limit where things can be dragged/dropped from/to

In my case, left to right and right to left is allowed. Within the left View is allowed. But reordering of the right is not enabled. That's just a repository for available Components, it's not returned to the server, so no point in being able to move inside it.

A DDView is a form field if the markup and script is written by my JSP tags handling system.

The class itself is ready to behave like a form field (it has the correct methods so that a Form can work with it), but obviously, nothing will be submitted by default because Ext.form.Action.submit just collects the DOM form's input values.

My tags handler adds a submit listener to the browser side ComponentPanel widget (a proprietary widget we use to contain application Components) which tags on the getValue() of the DDView as an extra param.

So internally, we use <aspicio:view> as an input field.

You would use http://extjs.com/deploy/ext-1.1.1/docs/output/Ext.form.BasicForm.html#event-beforeaction

to hook into the submit action and add an extra parameter which reflected the state of the View.

Troy Wolf
31 Aug 2007, 6:00 AM
Thank you and thank you. ~o)

So passing in my DDView list of values was as simple as adding a 'params' attribute to my submit button.
(Object names changed to protect the guilty.)


myForm.addButton('Apply', function() {
myForm.submit({
params:{
action:'DoSomeCustomAction',
myViewValues:myDDView.getValue()
},
waitMsg:'Processing data, please wait...',
reset:false,
scope:myForm,
success: function() {alert('success');}
});
});
So I end up with a form submitted and one of the values is the ordered list from my DDView as ordered by the user's Drag & Drop actions.

-----------------------------------------------
The minor problem I have now is that I want my DDView to be contained within a Form Fieldset that I've created with Ext, but my efforts so far have failed--the visible DDView displays outside my fieldset container. I could just create a normal HTML fieldset in my markup to contain my DDView, but....I want to have other Ext elements in my form and right now I'm still struggling with how to Ext-ize existing forms. I better understand how to create forms from scratch in Ext.

-----------------------------------------------
Regarding the dragGroup & dropGroup names--what you explain is exactly what I figured (as I explained). What I don't get is WHERE are those names assigned to any container? I see the names "availComponentDDGroup" and "subComponentDDGroup" being assigned as valid drag and drop targets but I don't actually see those targets created/defined anywhere. For example, a text search for "availComponentDDGroup" in your example only finds that text in the two DDView definitions---not anywhere in the markup or creation of Subcomponent and AvailableComponent containers. Obviously it works, so I'm not suggesting your code is wrong! I'm simply saying, I don't understand where those things are first created so that later you can use them as valid targets.

Thank you!

Animal
31 Aug 2007, 6:16 AM
The dd group names don't relate to containers. They are just names to which drag/drop zones can belong. So The left has a drag zone that belongs to groups availComponentDDGroup and subComponentDDGroup and a drop zone that belongs to both groups.

The right has a drag zone that belongs to availComponentDDGroup and a drop zone that belongs to subComponentDDGroup.

This limits what can be dragged/dropped into what.

Use the Fieldset's element as the View container.

Troy Wolf
31 Aug 2007, 6:46 AM
Use the Fieldset's element as the View container.

So if I have this form:

function initializeMyForm() {
var myForm= new Ext.form.Form({
url: 'myscript.php',
labelALign: 'right',
labelWidth: 75
});

myForm.fieldset(
{legend:'Hey, these things are grouped'}
);

myForm.addButton('Apply', function() {
myForm.submit({
params:{
action:'DoSomeCustomAction',
myViewValues:myDDView.getValue()
},
waitMsg:'Processing data, please wait...',
reset:false,
scope:myForm,
success: function() {alert('success');}
});
});;

myForm.render('myForm_div_container');
}

...and I want my DDView contained by that fieldset. How do I construct the DDView?

myDDView=new Ext.ux.DDView(SomethingThatReferencesMyFormFieldset,'<div id="myItem_{id}" class="">{myItemName}</div>',{

Animal
31 Aug 2007, 7:24 AM
http://extjs.com/deploy/ext-1.1.1/docs/output/Ext.form.Form.html#fieldset

It returns the FielSet.

@Jack, I just dropped an update into post #1.

I am comfortable with the selection model now.

The thing is, getDragData is called by the DragZone on mousedown, so it has to select the node it's on. It does that OK now.

But the existing model within the 1.1 View was strange. I have it working like file selection in Windoze explorer now, and I think that's what most prople will assume.

Troy Wolf
31 Aug 2007, 11:01 AM
Animal, as you know, I am using your code example from page 3 (http://extjs.com/forum/showthread.php?p=57292#post57292) where you show how to instantiate your DDView object. The code for the actual DDView object I'm keeping fresh from your initial post in this thread. Since you just updated it, per your last comment, I updated mine. I had to roll back. What I found was that I don't get any errors (I use Firebug), but there is a problem--at least with my implementation.

I have a list of three items in a DDView. The user can reorder the items. With the latest DDView code, the Drag does not always work. The small popup that shows the tree image and insertion arrow along with the text of the item (and image in your example) comes up with only the tree image sometimes. When that happens, you can drag, but you can't drop.

Example with old code:
http://www.troywolf.com/tmp/ddview_test1.html

Example with new code - drag & drop a few times to see the problem:
http://www.troywolf.com/tmp/ddview_test2.html

Perhaps the initialization example needs to change to be compatible with the object?

---------------------------------------------------
Animal, we are having a communication failure regarding the group names. I believe I understand what the intention of those group name assignments are--you have explained the same thing to me multiple times---and I thank you. You have explained that the group names tell the view what group(s) the drag & drop actions belong to. You go on to say that the group name "limits what can be dragged/dropped into what". My point/question is that it seems to me that the group name does nothing. I say this for 2 reasons. First, there is nothing in your example called/named "availComponentDDGroup". So how can you restrict a DDView's drag or drop to it? Second, in my code, I made up a completely bogus string for the dragGroup and dropGroup assignments--and my code works fine. You can see this in the examples at the links I posted above. I am ready to see the light. ~o)

Animal
31 Aug 2007, 11:34 AM
OK, grab the code from post #1 and see if that works.

See docs on groups: http://extjs.com/deploy/ext/docs/output/Ext.dd.DragSource.html#groups

Just try putting two DDViews side by side.

Give the left one a drag group "foo", the right a drop group "bar".

You will not be able to drag from left to right.

Troy Wolf
31 Aug 2007, 11:52 AM
OK, grab the code from post #1 and see if that works.
Yes! Fixed.


See docs on groups: http://extjs.com/deploy/ext/docs/output/Ext.dd.DragSource.html#groups

Just try putting two DDViews side by side.

Give the left one a drag group "foo", the right a drop group "bar".

You will not be able to drag from left to right.
Aha! So the dragGroup & dropGroup name(s) both defines and assigns the group!

I'm not sure why that was difficult for me to grasp. I guess in this everything is an object OOP world where we build each smallest unit as an object and then build the pieces together into bigger pieces, I expected a "group" to be an object that we build somewhere then assign the DDView object to the group object, yada, yada, yada.

------------------------------------------------

Using a DDView within an Ext Fieldset
If you look at this example, you'll see that the mouse cursor is a pointer.
http://www.troywolf.com/tmp/ddview_test2.html

Now look at this example. Only difference is that the DDView is contained inside an Ext Fieldset in an Ext Form.
http://www.troywolf.com/tmp/ddview_test3.html
Notice 2 things:

The mouse cursor is a text indicator while hovering over the sortable items--not as cool.
The fieldset's legend is gone even though it is defined and appears when the DDView is not inside the fieldset. I assume this is because technically, the legend is contained within the fieldset and when the DDView becomes contained in the fieldset, it overwrites everything in there?

Animal
31 Aug 2007, 12:21 PM
Yes, it looks like the View empties its container first. Oh well, you'll have to insert a div inside your FieldSet, and render the View inside that.

It's up to you to style the cursor any way you want. Just apply a class to the View container, set the cursor:<whatever you want> for that class, and off you go.

Animal
31 Aug 2007, 12:24 PM
You need important on your drop below and drop above classes, you're losing the visual insertion indicator:



.x-view-drag-insert-above {
border-top:1px dotted #3366CC !important;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366CC !important;
}

Troy Wolf
31 Aug 2007, 12:32 PM
Wow. I've been writing and copying CSS for years--I've never seen "!important". I added it--nice touch. FYI, the reason I did not have it is that your page 3 example does not have it. (I know you don't usually update that.)

Thanks!

Animal
31 Aug 2007, 12:46 PM
CSS selectors have precedence. On your page, it looks like there was a selector with a higher pecedence setting the borders, so the insertion point indicators never got shown. "!important" overrides precedence.

http://www.w3.org/TR/2005/WD-CSS21-20050613/cascade.html#cascading-order

Animal
3 Sep 2007, 12:20 AM
Latest version dropped into post #1.

Now correctly indicates that dropping a single node above or below itself is invalid.

Previously, this action was rejected at drop time, but now, the "No Drop" icon is displayed when the drop insertion point is above or below the node being dragged.

MaximGB
3 Sep 2007, 10:02 PM
Hi, Animal.

Nice extension, thanks. I have one little addition - I think that n.cloneNode(true) call in the getDragData() method isn't necessary since the cloneNode() called in the Ext.dd.DragZone::onInitDrag(). Without cloneNode() call the drag proxy will move (with animate) to the dragged element origin if the drop was on the invalid target, now it just moves to the 0,0 (upper left) screen corner. Also I think (for the same reason) that the multi-proxy div should be a child of the view container.

MaximGB
3 Sep 2007, 11:42 PM
Got one question...

I have a tree panel with ddGroup option set to 'tree' and DDView with:
dropGroup : 'images',
dragGroup : 'images,tree'
options.

And view don't want to drop on self :(, only on the tree. There is a green mark sign on the proxy when I drag the images over the view, but no above/below visuals, and after drop view doesn't rearrange the nodes....

http://max-bazhenov.com/temp/DDView-trouble.jpg

Animal
4 Sep 2007, 12:04 AM
Well spotted! I'll take the clone out, and do some testing! That flying up to 0,0 has been bugging me for a while.

If your View nodes float, you might have to extend the class, or use the code posted by someone else I seem to remember in the Extensions folder.

The class calculates the drop point as only "above" or "below". It assumes the View nodes are block elements which do not float, but flow down the container.

I suppose it should test whether the node floats (test the style.float/CSSFloat property), and if so, also check left/right pixel position, and return "left" or "right", or "above" or "below".

MaximGB
4 Sep 2007, 12:09 AM
I've done this, just overrode the getDropPoint with:


getDropPoint : function(e, n, dd)
{
if (n == this.el.dom) {
return "above";
}
var l = Ext.lib.Dom.getX(n), r = l + n.offsetWidth;
var c = l + (r - l) / 2;
var x = Ext.lib.Event.getPageX(e);

if(x <= c) {
return "above";
}else{
return "below";
}
}

Now it checks horizontal drop point position, but the result is the same. It works with single dragGroup, but doesn't work with multiple drag groups, I also overrode onNodeOver method (for testing) and with multiple drag groups it was called never.

Animal
4 Sep 2007, 12:39 AM
I removed the clone, and that worked.

However maknig the multi proxy a child of the View's container means that failed drags cause that node to be added to the View.

Try dropping this in:



getDragData : function(e) {
var target = this.findItemFromChild(e.getTarget());
if(target) {
if (!this.isSelected(target)) {
delete this.ignoreNextClick;
this.onItemClick(target, this.indexOf(target), e);
this.ignoreNextClick = true;
}
var dragData = {
sourceView: this,
viewNodes: [],
records: [],
copy: this.copy || (this.allowCopy && e.ctrlKey)
};
if (this.getSelectionCount() == 1) {
var i = this.getSelectedIndexes()[0];
dragData.viewNodes.push(dragData.ddel = this.getNode(i));
dragData.records.push(this.store.getAt(i));
} else {
dragData.ddel = this.getEl().createChild({cls:'multi-proxy'}, null, true);
this.collectSelection(dragData);
}
return dragData;
}
return false;
},


to see what I mean.



Re the multiple groups. It should split that:



if (this.dropGroup) {
this.setDroppable(this.dropGroup.split(","));
}


And add the drop zone to all groups. Step through the setDraggable and asetDroppable code.

It certainly works for me. I have



var view=new Ext.ux.DDView('AspicioForm1188895085234View5','<div id=\u0027subcomponent_{id}\u0027 class
=\u0027Subcomponent\u0027><img align=\u0027top\u0027 height=\u002716px\u0027 width=\u002716px\u0027 src
=\u0027{entityImageUrl}\u0027>{componentDescription}</div>',{
isFormField:true,
name:'subComponents',
dragGroup:'availComponentDDGroup,subComponentDDGroup',
dropGroup:'availComponentDDGroup,subComponentDDGroup',
selectedClass: 'x-combo-selected',
jsonRoot: 'collection',
store: ds
});


And it can drop on itself, or back to the "availableComponents" View.

MaximGB
4 Sep 2007, 12:50 AM
You have the same groups on dragGroup and dropGroup


dragGroup:'availComponentDDGroup,subComponentDDGroup',
dropGroup:'availComponentDDGroup,subComponentDDGroup'

When I set dragGroup and dropGroup view options to


dragGroup:'images,tree',
dropGroup:'images,tree'

The view start to work as it's expected to work, but as side effect now the user can drop node from tree into view, but it's wrong for my app.

Animal
4 Sep 2007, 12:51 AM
Remove 'tree' from the dropGroup

MaximGB
4 Sep 2007, 1:01 AM
This was the first I've done ;) since I understand what dropGroup and dragGroup are for (I've red the comments for setDraggable() and setDroppable() methods ;). But as soon as I remove the tree from the drop group the view starts to behaves wrong....

Animal
4 Sep 2007, 1:02 AM
OK, latest version dropped in post #1.

Both multi and single proxies both slide back to the correct place.

MaximGB
4 Sep 2007, 2:40 AM
I've solved my issue, I've changed the creation order of tree and view, now I create view then goes the tree. Nevertheless I think there is a bug most probably in Ext DD mechanics. During debug I've found that view didn't recieved onNodeOver notifications becourse it's DD target's 'isNotifyTarget' property was unset or set to false.

danh2000
7 Sep 2007, 1:55 AM
Hi Animal,

Great extension - thanks!

This is actually the first time I've used D&D... I've searched the forums and examples but am struggling with a couple of things:

1. I'm using your component to drop a view node onto a tree. The 'onbeforenodedrop' event fires perfectly, but the 'nodedrop' event is never triggered..

2. Also, I'm using the 'onbeforenodedrop' event to display a context menu on the tree (to select 'move' or 'copy'), but every time I drop a node and the context menu displays, the view node animates back to it's original position.

How do I prevent the animation back to the original position in the view? I want the ghost to disappear when the context menu is displayed.

Thanks in advance,

PS. I've updated my code with the code in the first post and I'm now getting an error during the onDblClick function.. I get:

targets.contains is not a function

any ideas?

Animal
7 Sep 2007, 3:53 AM
If you use allowCopy:true, in the config, then using ctrl+drag will set the copy flag in the drag data, and your implementation of getTreeNode which you supply will be able to interogate that, and know whether to remove the element from the source View or not.

Animal
7 Sep 2007, 3:59 AM
You just need to add the contains method to the Array prototype. It's in a post on this thread somewhere.


The only event my UX fires is "drop".

You don't need to subscribe to any events - just add a getTreeNode function to the View's dragZone eg:



/** Implement the tree DD interface in our DDView. */
Ext.apply(myDDView.dragZone, {
// this method is called by the TreeDropZone after a node drop
// to get the new tree node.
getTreeNode : function(data, targetNode, point, e){
var folderNode = ((point == "above") || (point == "below")) ? targetNode.parentNode : targetNode;
if (folderNode.isRoot) {
return false;
}
var treeNodes = [];
var recs = this.dragData.records;
for (var i = 0, len = recs.length; i < len; i++){
var r = recs[i];

// See if an item for this component exists.
var dup = folderNode.findChildBy(function(node){
return (node.attributes.id == r.id);
});
if (dup) {
dup.ensureVisible(function() {
var a = Ext.get(dup.getUI().anchor);
a.scrollIntoView(dup.getOwnerTree().getEl());
a.frame('red', 1);
fcl.util.displayMessage("User already allocated to Menu", fcl.util.ERROR, 5);
});
} else {
treeNodes.push(new Ext.tree.TreeNode({
userId: r.id,
icon: r.get("entityImageUrl"),
text: r.get("knownAs"),
className: "User",
leaf:true
}));
if (!data.copy) {
myDDView.store.remove(r);
}
}
}
return (treeNodes.length > 0) ? treeNodes : false;
}
});


Obviously, when you loop through the records, and create tree nodes from each one, you'll be using different fields that I did - you won't have "knownAs" in your records stc, but I'm sure you get the idea.

Animal
7 Sep 2007, 11:31 PM
OK, the next step is to add some kind of option to have visual highlighting of the View when hovering over with a valid drop.

Visual highlighting when a drop has taken place.

An appendOnly config option.

A float:true config option which tells the View that the elements float rather than stack.

And then an Ext standard example page to drop into the examples directory.

I've got gardening to do this weekend, but I'll try to keep developing it.

MaximGB
9 Sep 2007, 2:57 AM
Hi, Animal.

When you'll finish with flowers irrigation please add 'selectionchange' event firing in DDView::deselect(). ;)

Animal
9 Sep 2007, 8:55 AM
I don't think this class will be finished until it's extending Ext 2.0's DataView.

I think that has a seperate selection model which exposes events, so the DDView, and any client code would use the selection model.

Animal
10 Sep 2007, 8:29 AM
Here's an example which drops into examples/view.

It is an attempt to demonstrate the DDView in terms of a business case, trying to use assets that are in the ext directory.

So its a photogtraphy web site, selling prints of photos taken at an event.



<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<style type="text/css">
* {
font-family:tahoma,arial,helvetica,sans-serif;
}

.x-form-view {
cursor:default;
background-color:#fff;
}

.available-photo {
float:left;
border:3px solid #dddddd!important;
margin-top:3px;
margin-left:3px;
cursor:pointer;
}

.photo-for-printing {
float:left;
border:3px solid #dddddd!important;
margin-top:3px;
margin-left:3px;
cursor:pointer;
}

.available-photo.x-view-selected, .photo-for-printing.x-view-selected {
background:#C3DAF9 none repeat scroll 0%;
border:3px solid #6593CF!important;
}

.photo-for-printing img {
margin-bottom:2px
}

.photo-for-printing select {
width:70px
}

.x-view-drag-insert-above {
border-top:1px dotted #3366cc!important;
}
.x-view-drag-insert-left {
border-left:1px dotted #3366cc!important;
}
.x-view-drag-insert-right {
border-right:1px dotted #3366cc!important;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc!important;
}

.container-over {
background-color:pink
}

</style>
<link rel="stylesheet" type="text/css" href="../../resources/css/ext-all.css" />
<link rel="stylesheet" type="text/css" href="../examples.css" />
<script type="text/javascript" src="../../adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="../../ext-all-debug.js"></script>
<script type="text/javascript">
Array.prototype.contains = function(element) {
for (var i = 0; i < this.length; i++) {
if (this[i] == element) {
return true;
}
}
return false;
};

Ext.namespace("Ext.ux");

/**
* @class Ext.ux.DDView
* A DnD enabled version of Ext.View.
* @param {Element/String} container The Element in which to create the View.
* @param {String} tpl The template string used to create the markup for each element of the View
* @param {Object} config The configuration properties. These include all the config options of
* {@link Ext.View} plus some specific to this class.<br>
* <p>
* Drag/drop is implemented by adding {@link Ext.data.Record}s to the target DDView. If copying is
* not being performed, the original {@link Ext.data.Record} is removed from the source DDView.<br>
* <p>
* The following extra CSS rules are needed to provide insertion point highlighting:<pre><code>
.x-view-drag-insert-above {
border-top:1px dotted #3366cc;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc;
}
</code></pre>
*
*/
Ext.ux.DDView = function(container, tpl, config) {
if (this.classRe.test(tpl)) {
tpl = tpl.replace(this.classRe, 'class=$1x-combo-list-item $2$1');
} else {
tpl = tpl.replace(this.tagRe, '$1 class="x-combo-list-item" $2');
}

Ext.ux.DDView.superclass.constructor.apply(this, arguments);
this.getEl().setStyle("outline", "0px none");
this.getEl().unselectable();
if (this.dragGroup) {
this.setDraggable(this.dragGroup.split(","));
}
if (this.dropGroup) {
this.setDroppable(this.dropGroup.split(","));
}
if (this.deletable) {
this.setDeletable(this.deletable);
}
this.isDirtyFlag = false;
this.addEvents({
"drop" : true
});
};

Ext.extend(Ext.ux.DDView, Ext.View, {
/** @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone. */
/** @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone. */
/** @cfg {Boolean} copy Causes drag operations to copy nodes rather than move. */
/** @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move. */

isFormField: true,

classRe: /class=(['"])(.*)\1/,

tagRe: /(<\w*)(.*?>)/,

reset: Ext.emptyFn,

clearInvalid: Ext.form.Field.prototype.clearInvalid,

validate: function() {
return true;
},

destroy: function() {
this.purgeListeners();
this.getEl().removeAllListeners();
this.getEl().remove();
if (this.dragZone) {
if (this.dragZone.destroy) {
this.dragZone.destroy();
}
}
if (this.dropZone) {
if (this.dropZone.destroy) {
this.dropZone.destroy();
}
}
},

// Override: Don't select when input elements are clicked on
onClick: function(e) {
if (e.getTarget().tagName.match(/^(:?select|input)$/i)) {
e.stopEvent();
return;
}
Ext.ux.DDView.superclass.onClick.apply(this, arguments);
},

/** Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}. */
getName: function() {
return this.name;
},

/** Loads the View from a JSON string representing the Records to put into the Store. */
setValue: function(v) {
if (!this.store) {
throw "DDView.setValue(). DDView must be constructed with a valid Store";
}
var data = {};
data[this.store.reader.meta.root] = v ? [].concat(v) : [];
this.store.proxy = new Ext.data.MemoryProxy(data);
this.store.load();
},

/** @return {String} a parenthesised list of the ids of the Records in the View. */
getValue: function() {
var result = '(';
this.store.each(function(rec) {
result += rec.id + ',';
});
return result.substr(0, result.length - 1) + ')';
},

getIds: function() {
var i = 0, result = new Array(this.store.getCount());
this.store.each(function(rec) {
result[i++] = rec.id;
});
return result;
},

isDirty: function() {
return this.isDirtyFlag;
},

/**
* Part of the Ext.dd.DropZone interface. If no target node is found, the
* whole Element becomes the target, and this causes the drop gesture to append.
*/
getTargetFromEvent : function(e) {
var target = e.getTarget();
while ((target !== null) && (target.parentNode != this.el.dom)) {
target = target.parentNode;
}
if (!target) {
target = this.el.dom.lastChild || this.el.dom;
}
return target;
},

/**
* Create the drag data which consists of an object which has the property "ddel" as
* the drag proxy element.
*/
getDragData : function(e) {
var target = this.findItemFromChild(e.getTarget());
if(target) {
if (!this.isSelected(target)) {
delete this.ignoreNextClick;
this.onItemClick(target, this.indexOf(target), e);
this.ignoreNextClick = true;
}
var dragData = {
sourceView: this,
viewNodes: [],
records: [],
copy: this.copy || (this.allowCopy && e.ctrlKey)
};
if (this.getSelectionCount() == 1) {
var i = this.getSelectedIndexes()[0];
var n = this.getNode(i);
dragData.viewNodes.push(dragData.ddel = n);
dragData.records.push(this.store.getAt(i));
dragData.repairXY = Ext.fly(n).getXY();
} else {
if (this.dragZone.dragData.ddel) {
var d = Ext.get(this.dragZone.dragData.ddel);
if (d.hasClass('multi-proxy')) {
d.update('');
}
}
dragData.ddel = document.createElement('div');
dragData.ddel.className = 'multi-proxy';
this.collectSelection(dragData);
}
return dragData;
}
return false;
},

// override the default repairXY.
getRepairXY : function(e){
return this.dragData.repairXY;
},

/** Put the selections into the records and viewNodes Arrays. */
collectSelection: function(data) {
data.repairXY = Ext.fly(this.getSelectedNodes()[0]).getXY();
if (this.preserveSelectionOrder === true) {
Ext.each(this.getSelectedIndexes(), function(i) {
var n = this.getNode(i);
var dragNode = n.cloneNode(true);
dragNode.id = Ext.id();
data.ddel.appendChild(dragNode);
data.records.push(this.store.getAt(i));
data.viewNodes.push(n);
}, this);
} else {
var i = 0;
this.store.each(function(rec){
if (this.isSelected(i)) {
var n = this.getNode(i);
var dragNode = n.cloneNode(true);
dragNode.id = Ext.id();
data.ddel.appendChild(dragNode);
data.records.push(this.store.getAt(i));
data.viewNodes.push(n);
}
i++;
}, this);
}
},

/** Specify to which ddGroup items in this DDView may be dragged. */
setDraggable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDraggable, this);
return;
}
if (this.dragZone) {
this.dragZone.addToGroup(ddGroup);
} else {
this.dragZone = new Ext.dd.DragZone(this.getEl(), {
containerScroll: true,
ddGroup: ddGroup
});
// Draggability implies selection. DragZone's mousedown selects the element.
if (!this.multiSelect) { this.singleSelect = true; }

// Wire the DragZone's handlers up to methods in *this*
this.dragZone.getDragData = this.getDragData.createDelegate(this);
this.dragZone.getRepairXY = this.getRepairXY;
this.dragZone.onEndDrag = this.onEndDrag;
}
},

/** Specify from which ddGroup this DDView accepts drops. */
setDroppable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDroppable, this);
return;
}
if (this.dropZone) {
this.dropZone.addToGroup(ddGroup);
} else {
this.dropZone = new Ext.dd.DropZone(this.getEl(), {
owningView: this,
containerScroll: true,
ddGroup: ddGroup
});

// Wire the DropZone's handlers up to methods in *this*
this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this);
this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this);
this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this);
this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this);
this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this);
}
},

/** Decide whether to drop above or below a View node. */
getDropPoint : function(e, n, dd){
if (n == this.el.dom) { return "above"; }
var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight;
var c = t + (b - t) / 2;
var y = Ext.lib.Event.getPageY(e);
if(y <= c) {
return "above";
}else{
return "below";
}
},

isValidDropPoint: function(pt, n, data) {
if (data.viewNodes.length > 1) {
return true;
}
var d = data.viewNodes[0];
if (d == n) {
return false;
}
if ((pt == "below") && (n.nextSibling == d)) {
return false;
}
if ((pt == "above") && (n.previousSibling == d)) {
return false;
}
return true;
},

onNodeEnter : function(n, dd, e, data){
if (this.highlightColor && (data.sourceView != this)) {
this.el.highlight(this.highlightColor);
}
return false;
},

onNodeOver : function(n, dd, e, data){
var dragElClass = this.dropNotAllowed;
var pt = this.getDropPoint(e, n, dd);
if (this.isValidDropPoint(pt, n, data)) {
if (this.appendOnly) {
return "x-tree-drop-ok-below";
}

// set the insert point style on the target node
if (pt) {
var targetElClass;
if (pt == "above"){
dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";
targetElClass = "x-view-drag-insert-above";
} else {
dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";
targetElClass = "x-view-drag-insert-below";
}
if (this.lastInsertClass != targetElClass){
Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass);
this.lastInsertClass = targetElClass;
}
}
}
return dragElClass;
},

onNodeOut : function(n, dd, e, data){
this.removeDropIndicators(n);
},

onNodeDrop : function(n, dd, e, data){
var pt = this.getDropPoint(e, n, dd);
var insertAt = (this.appendOnly || (n == this.el.dom)) ? this.nodes.length : n.nodeIndex;
if (pt == "below") {
insertAt++;
}

// Validate if dragging within a DDView
if (data.sourceView == this) {
// If the first element to be inserted below is the target node, remove it
if (pt == "below") {
if (data.viewNodes[0] == n) {
data.viewNodes.shift();
}
} else { // If the last element to be inserted above is the target node, remove it
if (data.viewNodes[data.viewNodes.length - 1] == n) {
data.viewNodes.pop();
}
}

// Nothing to drop...
if (!data.viewNodes.length) {
return false;
}

// If we are moving DOWN, then because a store.remove() takes place first,
// the insertAt must be decremented.
if (insertAt > this.store.indexOf(data.records[0])) {
insertAt--;
}
}

// Dragging from a Tree. Use the Tree's recordFromNode function.
if (data.node instanceof Ext.tree.TreeNode) {
var r = data.node.getOwnerTree().recordFromNode(data.node);
if (r) {
data.records = [ r ];
}
}

if (!data.records) {
alert("Programming problem. Drag data contained no Records");
return false;
}

for (var i = 0; i < data.records.length; i++) {
var r = data.records[i];
var dup = this.store.getById(r.id);
if (dup && (dd != this.dragZone)) {
Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1);
} else {
if (data.copy) {
var c = r.copy();
if (this.fireEvent("drop", this, n, dd, e, data, c) !== false) {
this.store.insert(insertAt++, c);
}
} else {
if (this.fireEvent("drop", this, n, dd, e, data, r) !== false) {
if (data.sourceView) {
data.sourceView.isDirtyFlag = true;
data.sourceView.store.remove(r);
}
this.store.insert(insertAt++, r);
}
}
this.isDirtyFlag = true;
}
}
return true;
},

// Ensure the multi proxy is removed
onEndDrag: function(data, e) {
if (this.dragData.ddel) {
var d = Ext.get(this.dragData.ddel);
if (d.hasClass('multi-proxy')) {
d.remove();
}
}
},

removeDropIndicators : function(n){
if(n){
Ext.fly(n).removeClass([
"x-view-drag-insert-above",
"x-view-drag-insert-left",
"x-view-drag-insert-right",
"x-view-drag-insert-below"]);
this.lastInsertClass = "_noclass";
}
},

/**
* Utility method. Add a delete option to the DDView's context menu.
* @param {String} imageUrl The URL of the "delete" icon image.
*/
setDeletable: function(imageUrl) {
if (!this.singleSelect && !this.multiSelect) {
this.singleSelect = true;
}
var c = this.getContextMenu();
this.contextMenu.on("itemclick", function(item) {
switch (item.id) {
case "delete":
c.hide();
this.remove(this.getSelectedIndexes());
break;
}
}, this);
this.contextMenu.add({
icon: imageUrl,
id: "delete",
text: "Delete Item"
});
},

/** Return the context menu for this DDView. */
getContextMenu: function() {
if (!this.contextMenu) {
// Create the View's context menu
this.contextMenu = new Ext.menu.Menu({
id: this.id + "-contextmenu"
});
this.el.on("contextmenu", this.showContextMenu, this);
}
return this.contextMenu;
},

disableContextMenu: function() {
if (this.contextMenu) {
this.el.un("contextmenu", this.showContextMenu, this);
}
},

showContextMenu: function(e, item) {
item = this.findItemFromChild(e.getTarget());
if (item) {
e.stopEvent();
this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true);
this.contextMenu.showAt(e.getXY());
}
},

/**
* Remove {@link Ext.data.Record}s at the specified indices.
* @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove.
*/
remove: function(selectedIndices) {
selectedIndices = [].concat(selectedIndices);
for (var i = 0; i < selectedIndices.length; i++) {
var rec = this.store.getAt(selectedIndices[i]);
this.store.remove(rec);
}
},

/**
* Double click fires the event, but also, if this is draggable, and there is only one other
* related DropZone that is in another DDView, it drops the selected node on that DDView.
*/
onDblClick : function(e){
var item = this.findItemFromChild(e.getTarget());
if(item){
if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) {
return false;
}
if (this.dragGroup) {
var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true);

// Remove instances of this View's DropZone
while (targets.contains(this.dropZone)) {
targets.remove(this.dropZone);
}

// If there's only one other DropZone, and it is owned by a DDView, then drop it in
if ((targets.length == 1) && (targets[0].owningView)) {
this.dragZone.cachedTarget = null;
var el = Ext.get(targets[0].getEl());
var box = el.getBox(true);
targets[0].onNodeDrop(el.dom, {
target: el.dom,
xy: [box.x, box.y + box.height - 1]
}, null, this.getDragData(e));
}
}
}
},

onItemClick : function(item, index, e){
// The DragZone's mousedown->getDragData already handled selection
if (this.ignoreNextClick) {
delete this.ignoreNextClick;
return;
}

if(this.fireEvent("beforeclick", this, index, item, e) === false){
return false;
}
if(this.multiSelect || this.singleSelect){
if(this.multiSelect && e.shiftKey && this.lastSelection){
this.select(this.getNodes(this.indexOf(this.lastSelection), index), false);
} else if (this.isSelected(item) && e.ctrlKey) {
this.deselect(item);
}else{
this.deselect(item);
this.select(item, this.multiSelect && e.ctrlKey);
this.lastSelection = item;
}
e.preventDefault();
}
return true;
},

deselect : function(nodeInfo){
var node = this.getNode(nodeInfo);
if(node && this.isSelected(node)){
Ext.fly(node).removeClass(this.selectedClass);
this.selections.remove(node);
}
}
});

function initializePage() {
var collection=[
{'id':'1','thumbUrl':'images/thumbs/dance_fever.jpg', copies:1, sizeCode:0},
{'id':'2','thumbUrl':'images/thumbs/gangster_zack.jpg', copies:1, sizeCode:0},
{'id':'3','thumbUrl':'images/thumbs/kids_hug2.jpg', copies:1, sizeCode:0},
{'id':'4','thumbUrl':'images/thumbs/kids_hug.jpg', copies:1, sizeCode:0},
{'id':'5','thumbUrl':'images/thumbs/sara_pink.jpg', copies:1, sizeCode:0},
{'id':'6','thumbUrl':'images/thumbs/sara_pumpkin.jpg', copies:1, sizeCode:0},
{'id':'7','thumbUrl':'images/thumbs/sara_smile.jpg', copies:1, sizeCode:0},
{'id':'8','thumbUrl':'images/thumbs/up_to_something.jpg', copies:1, sizeCode:0},
{'id':'9','thumbUrl':'images/thumbs/zack.jpg', copies:1, sizeCode:0},
{'id':'10','thumbUrl':'images/thumbs/zack_dress.jpg', copies:1, sizeCode:0},
{'id':'11','thumbUrl':'images/thumbs/zack_hat.jpg', copies:1, sizeCode:0},
{'id':'12','thumbUrl':'images/thumbs/zack_sink.jpg', copies:1, sizeCode:0},
{'id':'13','thumbUrl':'images/thumbs/zacks_grill.jpg', copies:1, sizeCode:0}
];
var Photograph = Ext.data.Record.create([
{name: 'id'},
{name: 'thumbUrl'},
{name: 'copies'},
{name: 'sizeCode'},
]);
var photoReader = new Ext.data.JsonReader({
root: 'collection',
id: 'id'
}, Photograph);
var ds = new Ext.data.Store({
proxy: new Ext.data.MemoryProxy({collection:collection}),
reader: photoReader
});
var view = new Ext.ux.DDView('left-view-container','<div id="available-photo-{id}" class="available-photo"><img align="top" src="{thumbUrl}"></div>',{
name: 'photoSet',
multiSelect: true,
copy: true,
dragGroup: 'availPhotosDDGroup',
jsonRoot: 'collection',
store: ds
});
ds.load();

ds = new Ext.data.Store({
proxy: new Ext.data.MemoryProxy({collection:[]}),
reader: photoReader
});
purchased = new Ext.ux.DDView('right-view-container',
'<div id="selected-photo-{id}" class="photo-for-printing"><img align="top" src="{thumbUrl}">' +
'<div class="x-form-item"><label style="width:45px">Copies:</label><select name="photo-{id}-copies"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option></select></div>' +
'<div class="x-form-item"><label style="width:45px">Size:</label><select name="photo-{id}-size"><option value="0">18 x 12</option><option value="1">10 x 8</option><option value="0">8 x 6</option></select></div>' +
'</div>',
{
name: 'purchasedPrints',
dropGroup: 'availPhotosDDGroup',
jsonRoot: 'collection',
appendOnly: true,
deletable: "../shared/icons/fam/cross.gif",
store: ds
}
);
ds.load();

Ext.get("photo-selection").on("submit", function(e) {
e.stopEvent();
});
}
Ext.onReady(initializePage);
</script>
</head>
<body>
<p>
This demonstrates the drag/drop enabled Ext View. The View's template in the "for printing"
area includes input fields named "photo-" + the photograph's id + "-copies" and
"photo-" + the photograph's id + "-size" so the form is ready for submission to a server
with no complex processing.
<h1>Select Photographs for Printing</h1>
<form style="padding-top:20px;padding-left:10px" class="x-form x-box-blue" id="photo-selection">
<div class="x-form-ct x-form-column x-form-label-left">
<div class="x-box-tl"><div class="x-box-tr"><div class="x-box-tc"></div></div></div>
<div class="x-box-ml">
<div class="x-box-mr">
<div class="x-box-mc">
<h3>Your photographs</h3>
<div id="left-view-container" class="x-form-view" style="overflow:auto;height:420px"></div>
</div>
</div>
</div>
<div class="x-box-bl"><div class="x-box-br"><div class="x-box-bc"></div></div></div>
</div>
<div style="padding-left:10px" class="x-form-ct x-form-column x-form-label-left ">
<div class="x-box-tl"><div class="x-box-tr"><div class="x-box-tc"></div></div></div>
<div class="x-box-ml">
<div class="x-box-mr">
<div class="x-box-mc">
<h3>Selected for printing</h3>
<div id="right-view-container" class="x-form-view" style="overflow:auto;height:420px"></div>
</div>
</div>
</div>
<div class="x-box-bl"><div class="x-box-br"><div class="x-box-bc"></div></div></div>
</div>
<br>
<button type="submit">Submit</button>
</form>
</body>
</html>

xcipiy
12 Sep 2007, 11:23 PM
Hi Animal

This last example of yours does not load the images in IE7. Any idea why ?

Thanks

xcipiy
12 Sep 2007, 11:48 PM
Never mind I found it, there is a comma at the end of last image in collection variable: {'id':'13','thumbUrl':'images/thumbs/zacks_grill.jpg', copies:1, sizeCode:0},
which must be removed

Animal
13 Sep 2007, 12:38 AM
I edited the post and it's fixed for anyone else who wants to run it.

tjstuart
20 Sep 2007, 7:29 PM
Animal,

I am now using your DDView for Ext.ux.Multiselect/ItemSelector (see thread http://extjs.com/forum/showthread.php?t=13202).

I have an issue with it however ...

"When multiple items are dropped anywhere outside a valid drop zone a runtime error occurs (Ext.Element.fly(this.dragData.ddel) has no properties) then drag no longer works."

I am using the version of DDView you posted with your photo demo a while back as your latest version seems to maintain a strange visible list below the DDViews.

I have been known to be stupid before so I may be using DDView entirely incorrectly. Anyways details in the thread noted above.

Cheers

Animal
21 Sep 2007, 12:34 AM
Yes, that problem is fixed. I was being too clever, trying to use DomHelper to create the ddel, but that actually adds to the document, so the ddel became visible. Now, it just uses document.createElement(), so the ddel exists, but is not visible.

That latest version in the demo should work now.

galdaka
25 Sep 2007, 9:41 AM
What is the latest version of your example?

Thanks in advance,

tjstuart
25 Sep 2007, 7:48 PM
Yes, that problem is fixed. I was being too clever, trying to use DomHelper to create the ddel, but that actually adds to the document, so the ddel became visible. Now, it just uses document.createElement(), so the ddel exists, but is not visible.

That latest version in the demo should work now.

Animal,

I used the latest from your photo example and the "strange" visible selection list is now gone but I still have this issue ...

"When multiple items are dropped anywhere outside a valid drop zone a runtime error occurs (Ext.Element.fly(this.dragData.ddel) has no properties) then drag no longer works."

Perhaps I'm not using the correct version.

You can see it "breaking" in my Multiselect/ItemSelector example page @ http://www.figtreesystems.com/ext/ext-ux/1.1/Multiselect/Multiselect.html

To replicate (via ItemSelector example):-

1. Select two or more items from the "available" list.
2. Drag anywhere but the "selected" list
3. Attempt to again select some items from the "available" list
4. Error occurs and drag no longer works

Am I'm doing something wrong?

Animal
26 Sep 2007, 12:38 AM
There's a bug somewhere. I'll have a look into it when I get back to work next Monday.

tjstuart
26 Sep 2007, 5:21 PM
Animal,

With a view to getting DDView to work for Ext.ux.ItemSelector I've gone ahead and fixed the bug however unsure of the ramifications.

As this bug stops ItemSelector from working properly I will add the fix to the DDView that gets packaged up with the Multiselect/ItemSelector download.

Bug:-

1. 'onEndDrag' is run and delete is called on 'this.dragData.ddel'.

2. DragZone.afterRepair is automatically called and attempts to access 'this.dragData.ddel' (which no longer exists).

So all I did was comment out the line marked below.



onEndDrag: function(data, e) {
if (this.dragData.ddel) {
var d = Ext.get(this.dragData.ddel);
if (d.hasClass('multi-proxy')) {
d.remove();
//delete this.dragData.ddel; <-- removed this line
}
}
}

Animal
28 Sep 2007, 12:27 AM
OK, thanks for finding that. I'll edit the code.

Bill Sheppard
5 Oct 2007, 12:13 PM
Hi,

DDView works great for me in single selection mode. However, when I select more than on item in the view, the selected nodes start showing up on in the top left corner of the page, for each selection (but do select just fine in the DDView). In Firefox, the multiple selection drag works fine (except for the selected nodes showing in the top right corner). Bombs in IE6 in the handleMouseDown method of Ext.dd.DragSource.

My question is why the selected nodes are showing up (see screen shots). It happens when the nodes get added to the div with the multi-proxy class. If I set the the visibility of the div with the multi-proxy to false (via the style sheet), I don't see the selected nodes show up, but of course then the drag proxy doesn't display the items being dragged. I suspect the answer to that question will likely explain why IE6 chokes on it.

BTW, I'm using Ext 1.1.

Thanks in advance for any help! I have included screen shots, the html, and js files.

--Bill


Screen shots:
http://mvyaa.org/DDViewSingleSelect.gif

http://mvyaa.org/DDViewMultipleSelect.gif

html:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="../resources/css/ext-all.css">
<script type="text/javascript" src="../adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="../ext-all-debug.js"></script>
<script type="text/javascript" src="dd.js"></script>
<!-- A Localization Script File comes here -->
<script type="text/javascript">Ext.onReady(Tutorial.dd.init, Tutorial.dd);</script>
<title>Advanced Custom Drag &amp; Drop Tutorial</title>
<style type="text/css">
body {
font-size: 11px;
font-family: arial;
}
.track-title {
padding: 4px;
text-align: center;
font: bold 16px Arial;
background: black;
color: white;
}
.dd-ct {
position:absolute;
border: 1px solid silver;
width: 180px;
height: 300px;
top: 64px;
background-color: #ffffc0;
}
#dd1-ct {
left: 164px;
}
#dd2-ct {
left: 356px;
}
#dd3-ct {
left: 546px;
}
#dd4-ct {
left: 736px;
}
.dd-item {
height: 14px;
border: 1px solid #a0a0a0;
background-color: #c4d0ff;
vertical-align: middle;
cursor: move;
padding: 2px;
z-index: 1000;
margin: 2px;
}
.dd-ct .dd-item {
margin: 2px;
}
.dd-proxy {
opacity: 0.4;
-moz-opacity: 0.4;
filter: alpha(opacity=40);
}
.dd-over {
background-color: #ffff60;
}
.x-view-selected {
background-color: blue;
color: white;
}
.yard-sheet-unit {
font-weight: bold;
}

.x-view-drag-insert-above {
border-top:15px solid white;
}
.x-view-drag-insert-below {
border-bottom:15px solid white;
}
.multi-proxy {

}
</style>
</head>
<body>
<div style"color:white; background-color:white">
<div class="dd-ct" id="dd1-ct" >
<div class="track-title" style="text-align: center;">Track 1</div>
<div id="track_1" ></div>
</div>
<div class="dd-ct" id="dd2-ct">
<div class="track-title" style="text-align: center;">Track 2</div>
<div id="track_2" ></div>
</div>
<div class="dd-ct" id="dd3-ct">
<div class="track-title" style="text-align: center;">Track 3</div>
<div id="track_3""></div>
</div>
<div class="dd-ct" id="dd4-ct">
<div class="track-title" style="text-align: center;">Track 4</div>
<div id="track_4""></div>
</div>
</div>
</body>
</html>

Script
=====


/**
* Advanced_Custom Drag & Drop Tutorial
* by Jozef Sakalos, aka Saki
* http://extjs.com/learn/Tutorial:Advanced_Custom_Drag_and_Drop_Part_1
*/

// reference local blank image
Ext.BLANK_IMAGE_URL = '../resources/images/default/s.gif';

Ext.namespace('Tutorial', 'Tutorial.dd');

// create application
Tutorial.dd = function() {
// do NOT access DOM from here; elements don't exist yet

// private variables

var dragZone1, dragZone2, dragZone3, dropTarget1, dropTarget2, dropTarget3;
// private functions

function createMyGrid() {
// dummy data to be replaced by xml
var track_1_data = [['10001'],['10002'],['10003'],['10004'],['10005'],['10006'],['10007'],['10008']];
var track_2_data = [['20001'],['20002'],['20003'],['20004'],['20005'],['20006'],['20007'],['20008']];
var track_3_data = [['30001'],['30002'],['30003'],['30004'],['30005'],['30006'],['30007'],['30008']];
var track_4_data = [['40001'],['40002'],['40003'],['40004'],['40005'],['40006'],['40007'],['40008']];

createTrackView('track_1', track_1_data);
createTrackView('track_2', track_2_data);
createTrackView('track_3', track_3_data);
createTrackView('track_4', track_4_data);
}

function createTrackView(viewDiv, viewDataStore) {
var ds = new Ext.data.Store({
proxy: new Ext.data.MemoryProxy(viewDataStore),
reader: new Ext.data.ArrayReader({}, [
{id: "UnitNumber", name: 'UnitNumber'}
])
});
var view = new Ext.ux.DDView(viewDiv,
'<div class="yard-sheet-unit"> unit {UnitNumber} </div>', // auto create template
{
multiSelect: true,
dragGroup: 'group',
dropGroup: 'group',
preserveSelectionOrder: true,

notifyDrop: function(dd, e, data) {
alert('A notify drop has been recieved!');
return true;
},
store: ds
});
/*
view.on("click", function(vw, index, node, e){

vw.select(node);
//alert('Node "' + node.id + '" at index: ' + index + " was clicked, selectionCount = " + vw.getSelectionCount());
});
*/
ds.load();
}

// public space
return {
// public properties, e.g. strings to translate

// public methods
init: function() {
createMyGrid();
}
};
}(); // end of app



Ext.namespace("Ext.ux");

/**
* @class Ext.ux.DDView
* A DnD enabled version of Ext.View.
* @param {Element/String} container The Element in which to create the View.
* @param {String} tpl The template string used to create the markup for each element of the View
* @param {Object} config The configuration properties. These include all the config options of
* {@link Ext.View} plus some specific to this class.<br>
* <p>
* Drag/drop is implemented by adding {@link Ext.data.Record}s to the target DDView. If copying is
* not being performed, the original {@link Ext.data.Record} is removed from the source DDView.<br>
* <p>
* The following extra CSS rules are needed to provide insertion point highlighting:<pre><code>
.x-view-drag-insert-above {
border-top:1px dotted #3366cc;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc;
}
</code></pre>
*
*/
Ext.ux.DDView = function(container, tpl, config) {
if (this.classRe.test(tpl)) {
tpl = tpl.replace(this.classRe, 'class=$1x-combo-list-item $2$1');
} else {
tpl = tpl.replace(this.tagRe, '$1 class="x-combo-list-item"');
}

Ext.ux.DDView.superclass.constructor.apply(this, arguments);
this.getEl().setStyle("outline", "0px none");
this.getEl().unselectable();
if (this.dragGroup) {
this.setDraggable(this.dragGroup.split(","));
}
if (this.dropGroup) {
this.setDroppable(this.dropGroup.split(","));
}
if (this.deletable) {
this.setDeletable();
}
this.isDirtyFlag = false;
this.addEvents({
"drop" : true
});
};

Ext.extend(Ext.ux.DDView, Ext.View, {
/** @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone. */
/** @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone. */
/** @cfg {Boolean} copy Causes drag operations to copy nodes rather than move. */
/** @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move. */

isFormField: true,

classRe: /class=(['"])(.*)\1/,

tagRe: /<(\w*).*?>/,

reset: Ext.emptyFn,

clearInvalid: Ext.form.Field.prototype.clearInvalid,

validate: function() {
return true;
},

destroy: function() {
this.purgeListeners();
this.getEl().removeAllListeners();
this.getEl().remove();
if (this.dragZone) {
if (this.dragZone.destroy) {
this.dragZone.destroy();
}
}
if (this.dropZone) {
if (this.dropZone.destroy) {
this.dropZone.destroy();
}
}
},

/** Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}. */
getName: function() {
return this.name;
},

/** Loads the View from a JSON string representing the Records to put into the Store. */
setValue: function(v) {
if (!this.store) {
throw "DDView.setValue(). DDView must be constructed with a valid Store";
}
var data = {};
data[this.store.reader.meta.root] = v ? [].concat(v) : [];
this.store.proxy = new Ext.data.MemoryProxy(data);
this.store.load();
},

/** @return {String} a parenthesised list of the ids of the Records in the View. */
getValue: function() {
var result = '(';
this.store.each(function(rec) {
result += rec.id + ',';
});
return result.substr(0, result.length - 1) + ')';
},

getIds: function() {
var i = 0, result = new Array(this.store.getCount());
this.store.each(function(rec) {
result[i++] = rec.id;
});
return result;
},

isDirty: function() {
return this.isDirtyFlag;
},

/**
* Part of the Ext.dd.DropZone interface. If no target node is found, the
* whole Element becomes the target, and this causes the drop gesture to append.
*/
getTargetFromEvent : function(e) {
var target = e.getTarget();
while ((target !== null) && (target.parentNode != this.el.dom)) {
target = target.parentNode;
}
if (!target) {
target = this.el.dom.lastChild || this.el.dom;
}
return target;
},

/**
* Create the drag data which consists of an object which has the property "ddel" as
* the drag proxy element.
*/
getDragData : function(e) {
var target = this.findItemFromChild(e.getTarget());
if(target) {
if (!this.isSelected(target)) {
delete this.ignoreNextClick;
this.onItemClick(target, this.indexOf(target), e);
this.ignoreNextClick = true;
}
var dragData = {
sourceView: this,
viewNodes: [],
records: [],
copy: this.copy || (this.allowCopy && e.ctrlKey)
};
if (this.getSelectionCount() == 1) {
var i = this.getSelectedIndexes()[0];
var n = this.getNode(i);
dragData.viewNodes.push(dragData.ddel = n);
dragData.records.push(this.store.getAt(i));
dragData.repairXY = Ext.fly(n).getXY();
} else {
dragData.ddel = Ext.fly(document.body).createChild({cls:'multi-proxy'}, null, true);
this.collectSelection(dragData);
}
return dragData;
}
return false;
},

// override the default repairXY.
getRepairXY : function(e){
return this.dragData.repairXY;
},

// Ensure the multi proxy is removed
onEndDrag: function(data, e) {
var d = Ext.get(this.dragData.ddel);
if (d && d.hasClass("multi-proxy")) {
d.remove();
}
},

/** Put the selections into the records and viewNodes Arrays. */
collectSelection: function(data) {
data.repairXY = Ext.fly(this.getSelectedNodes()[0]).getXY();
if (this.preserveSelectionOrder === true) {
Ext.each(this.getSelectedIndexes(), function(i) {
var n = this.getNode(i);
var dragNode = n.cloneNode(true);
dragNode.id = Ext.id();
data.ddel.appendChild(dragNode);
data.records.push(this.store.getAt(i));
data.viewNodes.push(n);
}, this);
} else {
var i = 0;
this.store.each(function(rec){
if (this.isSelected(i)) {
var n = this.getNode(i);
var dragNode = n.cloneNode(true);
dragNode.id = Ext.id();
data.ddel.appendChild(dragNode);
data.records.push(this.store.getAt(i));
data.viewNodes.push(n);
}
i++;
}, this);
}
},

/** Specify to which ddGroup items in this DDView may be dragged. */
setDraggable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDraggable, this);
return;
}
if (this.dragZone) {
this.dragZone.addToGroup(ddGroup);
} else {
this.dragZone = new Ext.dd.DragZone(this.getEl(), {
containerScroll: true,
ddGroup: ddGroup
});
// Draggability implies selection. DragZone's mousedown selects the element.
if (!this.multiSelect) { this.singleSelect = true; }

// Wire the DragZone's handlers up to methods in *this*
this.dragZone.getDragData = this.getDragData.createDelegate(this);
this.dragZone.getRepairXY = this.getRepairXY;
this.dragZone.onEndDrag = this.onEndDrag;
}
},

/** Specify from which ddGroup this DDView accepts drops. */
setDroppable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDroppable, this);
return;
}
if (this.dropZone) {
this.dropZone.addToGroup(ddGroup);
} else {
this.dropZone = new Ext.dd.DropZone(this.getEl(), {
owningView: this,
containerScroll: true,
ddGroup: ddGroup
});

// Wire the DropZone's handlers up to methods in *this*
this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this);
this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this);
this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this);
this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this);
this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this);
}
},

/** Decide whether to drop above or below a View node. */
getDropPoint : function(e, n, dd){
if (n == this.el.dom) { return "above"; }
var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight;
var c = t + (b - t) / 2;
var y = Ext.lib.Event.getPageY(e);
if(y <= c) {
return "above";
}else{
return "below";
}
},

isValidDropPoint: function(pt, n, data) {
if (data.viewNodes.length > 1) {
return true;
}
var d = data.viewNodes[0];
if (d == n) {
return false;
}
if ((pt == "below") && (n.nextSibling == d)) {
return false;
}
if ((pt == "above") && (n.previousSibling == d)) {
return false;
}
return true;
},

onNodeEnter : function(n, dd, e, data){
return false;
},

onNodeOver : function(n, dd, e, data){
var dragElClass = this.dropNotAllowed;
var pt = this.getDropPoint(e, n, dd);
if (this.isValidDropPoint(pt, n, data)) {
// set the insert point style on the target node
if (pt) {
var targetElClass;
if (pt == "above"){
dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";
targetElClass = "x-view-drag-insert-above";
} else {
dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";
targetElClass = "x-view-drag-insert-below";
}
if (this.lastInsertClass != targetElClass){
Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass);
this.lastInsertClass = targetElClass;
}
}
}
return dragElClass;
},

onNodeOut : function(n, dd, e, data){
this.removeDropIndicators(n);
},

onNodeDrop : function(n, dd, e, data){
if (this.fireEvent("drop", this, n, dd, e, data) === false) {
return false;
}
var pt = this.getDropPoint(e, n, dd);
var insertAt = (n == this.el.dom) ? this.nodes.length : n.nodeIndex;
if (pt == "below") {
insertAt++;
}

// Validate if dragging within a DDView
if (data.sourceView == this) {
// If the first element to be inserted below is the target node, remove it
if (pt == "below") {
if (data.viewNodes[0] == n) {
data.viewNodes.shift();
}
} else { // If the last element to be inserted above is the target node, remove it
if (data.viewNodes[data.viewNodes.length - 1] == n) {
data.viewNodes.pop();
}
}

// Nothing to drop...
if (!data.viewNodes.length) {
return false;
}

// If we are moving DOWN, then because a store.remove() takes place first,
// the insertAt must be decremented.
if (insertAt > this.store.indexOf(data.records[0])) {
insertAt--;
}
}

// Dragging from a Tree. Use the Tree's recordFromNode function.
if (data.node instanceof Ext.tree.TreeNode) {
var r = data.node.getOwnerTree().recordFromNode(data.node);
if (r) {
data.records = [ r ];
}
}

if (!data.records) {
alert("Programming problem. Drag data contained no Records");
return false;
}

for (var i = 0; i < data.records.length; i++) {
var r = data.records[i];
var dup = this.store.getById(r.id);
if (dup && (dd != this.dragZone)) {
Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1);
} else {
if (data.copy) {
this.store.insert(insertAt++, r.copy());
} else {
if (data.sourceView) {
data.sourceView.isDirtyFlag = true;
data.sourceView.store.remove(r);
}
this.store.insert(insertAt++, r);
}
this.isDirtyFlag = true;
}
}
this.dragZone.cachedTarget = null;
return true;
},

removeDropIndicators : function(n){
if(n){
Ext.fly(n).removeClass([
"x-view-drag-insert-above",
"x-view-drag-insert-below"]);
this.lastInsertClass = "_noclass";
}
},

/**
* Utility method. Add a delete option to the DDView's context menu.
* @param {String} imageUrl The URL of the "delete" icon image.
*/
setDeletable: function(imageUrl) {
if (!this.singleSelect && !this.multiSelect) {
this.singleSelect = true;
}
var c = this.getContextMenu();
this.contextMenu.on("itemclick", function(item) {
switch (item.id) {
case "delete":
this.remove(this.getSelectedIndexes());
break;
}
}, this);
this.contextMenu.add({
icon: imageUrl || AU.resolveUrl("/images/delete.gif"),
id: "delete",
text: AU.getMessage("deleteItem")
});
},

/** Return the context menu for this DDView. */
getContextMenu: function() {
if (!this.contextMenu) {
// Create the View's context menu
this.contextMenu = new Ext.menu.Menu({
id: this.id + "-contextmenu"
});
this.el.on("contextmenu", this.showContextMenu, this);
}
return this.contextMenu;
},

disableContextMenu: function() {
if (this.contextMenu) {
this.el.un("contextmenu", this.showContextMenu, this);
}
},

showContextMenu: function(e, item) {
item = this.findItemFromChild(e.getTarget());
if (item) {
e.stopEvent();
this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true);
this.contextMenu.showAt(e.getXY());
}
},

/**
* Remove {@link Ext.data.Record}s at the specified indices.
* @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove.
*/
remove: function(selectedIndices) {
selectedIndices = [].concat(selectedIndices);
for (var i = 0; i < selectedIndices.length; i++) {
var rec = this.store.getAt(selectedIndices[i]);
this.store.remove(rec);
}
},

/**
* Double click fires the event, but also, if this is draggable, and there is only one other
* related DropZone that is in another DDView, it drops the selected node on that DDView.
*/
onDblClick : function(e){
var item = this.findItemFromChild(e.getTarget());
if(item){
if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) {
return false;
}
if (this.dragGroup) {
var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true);

// Remove instances of this View's DropZone
while (targets.contains(this.dropZone)) {
targets.remove(this.dropZone);
}

// If there's only one other DropZone, and it is owned by a DDView, then drop it in
if ((targets.length == 1) && (targets[0].owningView)) {
this.dragZone.cachedTarget = null;
var el = Ext.get(targets[0].getEl());
var box = el.getBox(true);
targets[0].onNodeDrop(el.dom, {
target: el.dom,
xy: [box.x, box.y + box.height - 1]
}, null, this.getDragData(e));
}
}
}
},

onItemClick : function(item, index, e){
// The DragZone's mousedown->getDragData already handled selection
if (this.ignoreNextClick) {
delete this.ignoreNextClick;
return;
}

if(this.fireEvent("beforeclick", this, index, item, e) === false){
return false;
}
if(this.multiSelect || this.singleSelect){
if(this.multiSelect && e.shiftKey && this.lastSelection){
this.select(this.getNodes(this.indexOf(this.lastSelection), index), false);
} else if (this.isSelected(item) && e.ctrlKey) {
this.deselect(item);
}else{
this.deselect(item);
this.select(item, this.multiSelect && e.ctrlKey);
this.lastSelection = item;
}
e.preventDefault();
}
return true;
},

deselect : function(nodeInfo){
var node = this.getNode(nodeInfo);
if(node && this.isSelected(node)){
Ext.fly(node).removeClass(this.selectedClass);
this.selections.remove(node);
}
}
});
// end of file

tjstuart
7 Oct 2007, 4:03 PM
Bill,

I also had the same problem. Read the discussion on pages 8 and 9 of this thread. If you urgently need it fixed then I have a good copy of DDView that I've used for the Multiselect/ItemSelector extension. You can grab it out of the archive ... http://www.figtreesystems.com/ext/ext-ux/1.1/Multiselect/MultiselectItemSelector-2.1.zip

Regards

bowa
8 Oct 2007, 6:26 AM
i am setting up a test with dragging nodes from a tree to a DDView

it works but while i drag over the DDView before i release the mouse button, firebug spews these error messages really fast ...


data.viewNodes has no properties
isValidDropPoint("below", div#selected-photo-67.x-combo-list-item, Object node=[Node 68] handles=[2] isHandle=false)DDView.js (line 272)
onNodeOver(div#selected-photo-67.x-combo-list-item, DDProxy files el=Object dragData=Object, Object browserEvent=Event mouseout button=0 type=mouseout, Object node=[Node 68] handles=[2] isHandle=false)DDView.js (line 295)
apply()ext-base.js (line 9)
DropZone(DDProxy files el=Object dragData=Object, Object browserEvent=Event mouseout button=0 type=mouseout, Object node=[Node 68] handles=[2] isHandle=false)ext-all.js (line 40)
DragSource(Object browserEvent=Event mouseout button=0 type=mouseout, "project-body")ext-all.js (line 37)
DragDrop(Object browserEvent=Event mouseout button=0 type=mouseout, false)ext-all.js (line 33)
apply()ext-base.js (line 9)
DragDrop(Object browserEvent=Event mouseout button=0 type=mouseout)ext-all.js (line 33)
EventManager(Object browserEvent=Event mouseout button=0 type=mouseout)ext-all.js (line 13)
getViewWidth(mousemove clientX=0, clientY=0)ext-base.js (line 10)
[Break on this error] if (data.viewNodes.length > 1) {

this is the code i use ...



Array.prototype.contains = function(element) {
for (var i = 0; i < this.length; i++) {
if (this[i] == element) {
return true;
}
}
return false;
};

Ext.onReady(function(){

var xt = Ext.tree;

Ext.QuickTips.init();

var cview = Ext.DomHelper.append('project-ct',
{cn:[{id:'main-tb'},{id:'project-body'}]}
);

var tb = new Ext.Toolbar('main-tb');
tb.add({
id: 'save',
text: 'Save',
disabled:true,
//handler:save,
cls:'x-btn-text-icon save',
tooltip:'Saves this project to the server'
},'-'
);

var layout = new Ext.BorderLayout('project-ct', {
west: {
split: true,
initialSize: 200,
minSize: 175,
maxSize: 400,
titlebar: true,
margins:{left:5,right:0,bottom:5,top:5}
},
center: {
title: 'Project',
margins:{left:0,right:5,bottom:5,top:5}
}
});

layout.batchAdd({
west: {
id: 'files',
autoCreate:true,
title:'Files',
autoScroll:true,
fitToFrame:true
},
center: {
el: cview,
autoScroll:true,
fitToFrame:true,
toolbar: tb,
resizeEl:'project-body'
}
});

var fileTree = new xt.TreePanel('files', {
animate:true,
loader: new xt.DWRTreeLoader({dwrCall:TreeHelper.getTreeNodes}),
enableDD: true,
ddGroup:'availableFilesDDGroup',
containerScroll:true,
rootVisible:true
});

new xt.TreeSorter(fileTree, {folderSort:true});

var fileTreeRoot = new xt.AsyncTreeNode({
text: 'My Files',
draggable:false,
id:'0'
});

fileTree.setRootNode(fileTreeRoot);

fileTree.recordFromNode = function(n) {
var result = new projectFiles.store.recordType({
id: n.id,
name: n.text
});
return result;
};


fileTree.on('beforemove', function(fileTreeRoot, parent, oldParent, newParent, index){
if(oldParent.id != newParent.id){
TreeHelper.moveNode(parent.id, oldParent.id, newParent.id);
}
}, this, true
);

fileTree.on({
startdrag:{fn:function(data, node){
this.dragZone.proxy.update(node.text);
}}
});

fileTree.render();
fileTreeRoot.expand();


// ddview

var collection = [
{'id':'1','name':'test 1'},
{'id':'2','name':'test 2'},
{'id':'3','name':'test 3'}
];

var File = Ext.data.Record.create([
{name: 'id'},
{name: 'name'}
]);

var fileReader = new Ext.data.JsonReader({
root: 'collection',
id: 'id'
}, File);

var ds = new Ext.data.Store({
proxy: new Ext.data.MemoryProxy({collection:collection}),
reader: fileReader
});

var projectFiles = new Ext.ux.DDView('project-body',
'<div id="selected-photo-{id}" class="project-file-details">{id}:{name}' +
'</div>',
{
name: 'projectFiles',
dragGroup: 'availableFilesDDGroup',
dropGroup: 'availableFilesDDGroup',
jsonRoot: 'collection',
//appendOnly: true,
//deletable: "./ext/examples/shared/icons/fam/cross.gif",
store: ds
}
);

ds.load();

});


perhaps this has something to do with it, but when i drag fromthe tree over to the DDView gets the icon that dropping there is not allowed, although it does work ... when i drag (reposition) inside the DDView i get the right icons.

bowa
11 Oct 2007, 7:19 AM
i tried to digg into it a bit more ... it goes wrong in this block of code :


isValidDropPoint: function(pt, n, data) {
if (data.viewNodes.length > 1) {
return true;
}
var d = data.viewNodes[0];
if (d == n) {
return false;
}
if ((pt == "below") && (n.nextSibling == d)) {
return false;
}
if ((pt == "above") && (n.previousSibling == d)) {
return false;
}
return true;
},

while dragging a node from a DDview then there is no problem. because that node has a 'viewNodes' property ... while when you drag from a tree, data has a 'node' property, but not 'viewNodes' ...

the drag and drop works, but it just spews errors. anyone has got drag from tree to ddview working without those errors ?

bowa
11 Oct 2007, 11:08 PM
changing this in Animals latest code fixed my issues.


isValidDropPoint: function(pt, n, data) {
if(data.viewNodes){ // added for dragging from tree to DDview
if (data.viewNodes.length > 1) {
return true;
}
var d = data.viewNodes[0];
if (d == n) {
return false;
}
if ((pt == "below") && (n.nextSibling == d)) {
return false;
}
if ((pt == "above") && (n.previousSibling == d)) {
return false;
}
} //added
return true;
},

checking if data.viewNodes exists and if not (means we are draging FROM a tree), just return true.

bowa
12 Oct 2007, 1:21 AM
sorry for 'spamming' this thread lately, but i like to post the solutions to the thread too if i find them myself, so people might stumble upon them when they search with similar problems.

but i found another really weird thing ... in Animals photo order application i added two radio buttons after those two select boxes.

the radio buttons just don't work, not in IE not in Firefox ...


purchased = new Ext.ux.DDView('right-view-container',
'<div id="selected-photo-{id}" class="photo-for-printing"><img align="top" src="{thumbUrl}">' +
'<div class="x-form-item"><label style="width:45px">Copies:</label><select name="photo-{id}-copies"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option></select></div>' +
'<div class="x-form-item"><label style="width:45px">Size:</label><select name="photo-{id}-size"><option value="0">18 x 12</option><option value="1">10 x 8</option><option value="0">8 x 6</option></select></div>' +
'<div class="x-form-item"><label style="width:45px">Double:</label><input type="radio" name="photo-{id}-double" value="1">yes<br/><input type="radio" name="photo-{id}-double" value="0">no</div>' +
'</div>',
{
name: 'purchasedPrints',
dropGroup: 'availPhotosDDGroup',
jsonRoot: 'collection',
appendOnly: true,
deletable: "./ext/examples/shared/icons/fam/cross.gif",
store: ds
}
);

added this to the bottom of the page just for reference ... to make sure the html works. and they do (toggle between selecting the one clicked, unselecting the other).


<div class="x-form-item"><label style="width:45px">Double:</label><input type="radio" name="photo-xx-double" value="1">yes<br/><input type="radio" name="photo-xx-double" value="0">no</div>

anyone has an idea what is causing this ?

Animal
12 Oct 2007, 2:11 AM
"don't work"? The radio inputs aren't there? Won't react to clicks?

What do they look like in the HTML tab of Firebug?

bowa
12 Oct 2007, 2:50 AM
they don't work, no errors, the input rounds get selected. (dotted line around the circle) but the radio button dont get filled if the other is already filled.

the generated html (firebug) is correct ... i copy pasted that outside of the DDView and there it works ok.

compare the behaviour of the radio buttons inside the DDView, and the one i put as a reference at the bottom of the page.



<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<style type="text/css">
* {
font-family:tahoma,arial,helvetica,sans-serif;
}

.x-form-view {
cursor:default;
background-color:#fff;
}

.available-photo {
float:left;
border:3px solid #dddddd!important;
margin-top:3px;
margin-left:3px;
cursor:pointer;
}

.photo-for-printing {
float:left;
border:3px solid #dddddd!important;
margin-top:3px;
margin-left:3px;
cursor:pointer;
}

.available-photo.x-view-selected, .photo-for-printing.x-view-selected {
background:#C3DAF9 none repeat scroll 0%;
border:3px solid #6593CF!important;
}

.photo-for-printing img {
margin-bottom:2px
}

.photo-for-printing select {
width:70px
}

.x-view-drag-insert-above {
border-top:1px dotted #3366cc!important;
}
.x-view-drag-insert-left {
border-left:1px dotted #3366cc!important;
}
.x-view-drag-insert-right {
border-right:1px dotted #3366cc!important;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc!important;
}

.container-over {
background-color:pink
}

</style>

<link rel="stylesheet" type="text/css" href="./ext/resources/css/ext-all.css" />
<link rel="stylesheet" type="text/css" href="./ext/examples.css" />
<script type="text/javascript" src="./ext/adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="./ext/ext-all.js"></script>
<script type="text/javascript" src="./ext/ext-all-debug.js"></script>
<script type="text/javascript">
Array.prototype.contains = function(element) {
for (var i = 0; i < this.length; i++) {
if (this[i] == element) {
return true;
}
}
return false;
};

Ext.namespace("Ext.ux");

/**
* @class Ext.ux.DDView
* A DnD enabled version of Ext.View.
* @param {Element/String} container The Element in which to create the View.
* @param {String} tpl The template string used to create the markup for each element of the View
* @param {Object} config The configuration properties. These include all the config options of
* {@link Ext.View} plus some specific to this class.<br>
* <p>
* Drag/drop is implemented by adding {@link Ext.data.Record}s to the target DDView. If copying is
* not being performed, the original {@link Ext.data.Record} is removed from the source DDView.<br>
* <p>
* The following extra CSS rules are needed to provide insertion point highlighting:<pre><code>
.x-view-drag-insert-above {
border-top:1px dotted #3366cc;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc;
}
</code></pre>
*
*/
Ext.ux.DDView = function(container, tpl, config) {
if (this.classRe.test(tpl)) {
tpl = tpl.replace(this.classRe, 'class=$1x-combo-list-item $2$1');
} else {
tpl = tpl.replace(this.tagRe, '$1 class="x-combo-list-item" $2');
}

Ext.ux.DDView.superclass.constructor.apply(this, arguments);
this.getEl().setStyle("outline", "0px none");
this.getEl().unselectable();
if (this.dragGroup) {
this.setDraggable(this.dragGroup.split(","));
}
if (this.dropGroup) {
this.setDroppable(this.dropGroup.split(","));
}
if (this.deletable) {
this.setDeletable(this.deletable);
}
this.isDirtyFlag = false;
this.addEvents({
"drop" : true
});
};

Ext.extend(Ext.ux.DDView, Ext.View, {
/** @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone. */
/** @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone. */
/** @cfg {Boolean} copy Causes drag operations to copy nodes rather than move. */
/** @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move. */

isFormField: true,

classRe: /class=(['"])(.*)\1/,

tagRe: /(<\w*)(.*?>)/,

reset: Ext.emptyFn,

clearInvalid: Ext.form.Field.prototype.clearInvalid,

validate: function() {
return true;
},

destroy: function() {
this.purgeListeners();
this.getEl().removeAllListeners();
this.getEl().remove();
if (this.dragZone) {
if (this.dragZone.destroy) {
this.dragZone.destroy();
}
}
if (this.dropZone) {
if (this.dropZone.destroy) {
this.dropZone.destroy();
}
}
},

// Override: Don't select when input elements are clicked on
onClick: function(e) {
if (e.getTarget().tagName.match(/^(:?select|input)$/i)) {
e.stopEvent();
return;
}
Ext.ux.DDView.superclass.onClick.apply(this, arguments);
},

/** Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}. */
getName: function() {
return this.name;
},

/** Loads the View from a JSON string representing the Records to put into the Store. */
setValue: function(v) {
if (!this.store) {
throw "DDView.setValue(). DDView must be constructed with a valid Store";
}
var data = {};
data[this.store.reader.meta.root] = v ? [].concat(v) : [];
this.store.proxy = new Ext.data.MemoryProxy(data);
this.store.load();
},

/** @return {String} a parenthesised list of the ids of the Records in the View. */
getValue: function() {
var result = '(';
this.store.each(function(rec) {
result += rec.id + ',';
});
return result.substr(0, result.length - 1) + ')';
},

getIds: function() {
var i = 0, result = new Array(this.store.getCount());
this.store.each(function(rec) {
result[i++] = rec.id;
});
return result;
},

isDirty: function() {
return this.isDirtyFlag;
},

/**
* Part of the Ext.dd.DropZone interface. If no target node is found, the
* whole Element becomes the target, and this causes the drop gesture to append.
*/
getTargetFromEvent : function(e) {
var target = e.getTarget();
while ((target !== null) && (target.parentNode != this.el.dom)) {
target = target.parentNode;
}
if (!target) {
target = this.el.dom.lastChild || this.el.dom;
}
return target;
},

/**
* Create the drag data which consists of an object which has the property "ddel" as
* the drag proxy element.
*/
getDragData : function(e) {
var target = this.findItemFromChild(e.getTarget());
if(target) {
if (!this.isSelected(target)) {
delete this.ignoreNextClick;
this.onItemClick(target, this.indexOf(target), e);
this.ignoreNextClick = true;
}
var dragData = {
sourceView: this,
viewNodes: [],
records: [],
copy: this.copy || (this.allowCopy && e.ctrlKey)
};
if (this.getSelectionCount() == 1) {
var i = this.getSelectedIndexes()[0];
var n = this.getNode(i);
dragData.viewNodes.push(dragData.ddel = n);
dragData.records.push(this.store.getAt(i));
dragData.repairXY = Ext.fly(n).getXY();
} else {
if (this.dragZone.dragData.ddel) {
var d = Ext.get(this.dragZone.dragData.ddel);
if (d.hasClass('multi-proxy')) {
d.update('');
}
}
dragData.ddel = document.createElement('div');
dragData.ddel.className = 'multi-proxy';
this.collectSelection(dragData);
}
return dragData;
}
return false;
},

// override the default repairXY.
getRepairXY : function(e){
return this.dragData.repairXY;
},

/** Put the selections into the records and viewNodes Arrays. */
collectSelection: function(data) {
data.repairXY = Ext.fly(this.getSelectedNodes()[0]).getXY();
if (this.preserveSelectionOrder === true) {
Ext.each(this.getSelectedIndexes(), function(i) {
var n = this.getNode(i);
var dragNode = n.cloneNode(true);
dragNode.id = Ext.id();
data.ddel.appendChild(dragNode);
data.records.push(this.store.getAt(i));
data.viewNodes.push(n);
}, this);
} else {
var i = 0;
this.store.each(function(rec){
if (this.isSelected(i)) {
var n = this.getNode(i);
var dragNode = n.cloneNode(true);
dragNode.id = Ext.id();
data.ddel.appendChild(dragNode);
data.records.push(this.store.getAt(i));
data.viewNodes.push(n);
}
i++;
}, this);
}
},

/** Specify to which ddGroup items in this DDView may be dragged. */
setDraggable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDraggable, this);
return;
}
if (this.dragZone) {
this.dragZone.addToGroup(ddGroup);
} else {
this.dragZone = new Ext.dd.DragZone(this.getEl(), {
containerScroll: true,
ddGroup: ddGroup
});
// Draggability implies selection. DragZone's mousedown selects the element.
if (!this.multiSelect) { this.singleSelect = true; }

// Wire the DragZone's handlers up to methods in *this*
this.dragZone.getDragData = this.getDragData.createDelegate(this);
this.dragZone.getRepairXY = this.getRepairXY;
this.dragZone.onEndDrag = this.onEndDrag;
}
},

/** Specify from which ddGroup this DDView accepts drops. */
setDroppable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDroppable, this);
return;
}
if (this.dropZone) {
this.dropZone.addToGroup(ddGroup);
} else {
this.dropZone = new Ext.dd.DropZone(this.getEl(), {
owningView: this,
containerScroll: true,
ddGroup: ddGroup
});

// Wire the DropZone's handlers up to methods in *this*
this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this);
this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this);
this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this);
this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this);
this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this);
}
},

/** Decide whether to drop above or below a View node. */
getDropPoint : function(e, n, dd){
if (n == this.el.dom) { return "above"; }
var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight;
var c = t + (b - t) / 2;
var y = Ext.lib.Event.getPageY(e);
if(y <= c) {
return "above";
}else{
return "below";
}
},

isValidDropPoint: function(pt, n, data) {
if (data.viewNodes.length > 1) {
return true;
}
var d = data.viewNodes[0];
if (d == n) {
return false;
}
if ((pt == "below") && (n.nextSibling == d)) {
return false;
}
if ((pt == "above") && (n.previousSibling == d)) {
return false;
}
return true;
},

onNodeEnter : function(n, dd, e, data){
if (this.highlightColor && (data.sourceView != this)) {
this.el.highlight(this.highlightColor);
}
return false;
},

onNodeOver : function(n, dd, e, data){
var dragElClass = this.dropNotAllowed;
var pt = this.getDropPoint(e, n, dd);
if (this.isValidDropPoint(pt, n, data)) {
if (this.appendOnly) {
return "x-tree-drop-ok-below";
}

// set the insert point style on the target node
if (pt) {
var targetElClass;
if (pt == "above"){
dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";
targetElClass = "x-view-drag-insert-above";
} else {
dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";
targetElClass = "x-view-drag-insert-below";
}
if (this.lastInsertClass != targetElClass){
Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass);
this.lastInsertClass = targetElClass;
}
}
}
return dragElClass;
},

onNodeOut : function(n, dd, e, data){
this.removeDropIndicators(n);
},

onNodeDrop : function(n, dd, e, data){
var pt = this.getDropPoint(e, n, dd);
var insertAt = (this.appendOnly || (n == this.el.dom)) ? this.nodes.length : n.nodeIndex;
if (pt == "below") {
insertAt++;
}

// Validate if dragging within a DDView
if (data.sourceView == this) {
// If the first element to be inserted below is the target node, remove it
if (pt == "below") {
if (data.viewNodes[0] == n) {
data.viewNodes.shift();
}
} else { // If the last element to be inserted above is the target node, remove it
if (data.viewNodes[data.viewNodes.length - 1] == n) {
data.viewNodes.pop();
}
}

// Nothing to drop...
if (!data.viewNodes.length) {
return false;
}

// If we are moving DOWN, then because a store.remove() takes place first,
// the insertAt must be decremented.
if (insertAt > this.store.indexOf(data.records[0])) {
insertAt--;
}
}

// Dragging from a Tree. Use the Tree's recordFromNode function.
if (data.node instanceof Ext.tree.TreeNode) {
var r = data.node.getOwnerTree().recordFromNode(data.node);
if (r) {
data.records = [ r ];
}
}

if (!data.records) {
alert("Programming problem. Drag data contained no Records");
return false;
}

for (var i = 0; i < data.records.length; i++) {
var r = data.records[i];
var dup = this.store.getById(r.id);
if (dup && (dd != this.dragZone)) {
Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1);
} else {
if (data.copy) {
var c = r.copy();
if (this.fireEvent("drop", this, n, dd, e, data, c) !== false) {
this.store.insert(insertAt++, c);
}
} else {
if (this.fireEvent("drop", this, n, dd, e, data, r) !== false) {
if (data.sourceView) {
data.sourceView.isDirtyFlag = true;
data.sourceView.store.remove(r);
}
this.store.insert(insertAt++, r);
}
}
this.isDirtyFlag = true;
}
}
return true;
},

// Ensure the multi proxy is removed
onEndDrag: function(data, e) {
if (this.dragData.ddel) {
var d = Ext.get(this.dragData.ddel);
if (d.hasClass('multi-proxy')) {
d.remove();
}
}
},

removeDropIndicators : function(n){
if(n){
Ext.fly(n).removeClass([
"x-view-drag-insert-above",
"x-view-drag-insert-left",
"x-view-drag-insert-right",
"x-view-drag-insert-below"]);
this.lastInsertClass = "_noclass";
}
},

/**
* Utility method. Add a delete option to the DDView's context menu.
* @param {String} imageUrl The URL of the "delete" icon image.
*/
setDeletable: function(imageUrl) {
if (!this.singleSelect && !this.multiSelect) {
this.singleSelect = true;
}
var c = this.getContextMenu();
this.contextMenu.on("itemclick", function(item) {
switch (item.id) {
case "delete":
c.hide();
this.remove(this.getSelectedIndexes());
break;
}
}, this);
this.contextMenu.add({
icon: imageUrl,
id: "delete",
text: "Delete Item"
});
},

/** Return the context menu for this DDView. */
getContextMenu: function() {
if (!this.contextMenu) {
// Create the View's context menu
this.contextMenu = new Ext.menu.Menu({
id: this.id + "-contextmenu"
});
this.el.on("contextmenu", this.showContextMenu, this);
}
return this.contextMenu;
},

disableContextMenu: function() {
if (this.contextMenu) {
this.el.un("contextmenu", this.showContextMenu, this);
}
},

showContextMenu: function(e, item) {
item = this.findItemFromChild(e.getTarget());
if (item) {
e.stopEvent();
this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true);
this.contextMenu.showAt(e.getXY());
}
},

/**
* Remove {@link Ext.data.Record}s at the specified indices.
* @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove.
*/
remove: function(selectedIndices) {
selectedIndices = [].concat(selectedIndices);
for (var i = 0; i < selectedIndices.length; i++) {
var rec = this.store.getAt(selectedIndices[i]);
this.store.remove(rec);
}
},

/**
* Double click fires the event, but also, if this is draggable, and there is only one other
* related DropZone that is in another DDView, it drops the selected node on that DDView.
*/
onDblClick : function(e){
var item = this.findItemFromChild(e.getTarget());
if(item){
if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) {
return false;
}
if (this.dragGroup) {
var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true);

// Remove instances of this View's DropZone
while (targets.contains(this.dropZone)) {
targets.remove(this.dropZone);
}

// If there's only one other DropZone, and it is owned by a DDView, then drop it in
if ((targets.length == 1) && (targets[0].owningView)) {
this.dragZone.cachedTarget = null;
var el = Ext.get(targets[0].getEl());
var box = el.getBox(true);
targets[0].onNodeDrop(el.dom, {
target: el.dom,
xy: [box.x, box.y + box.height - 1]
}, null, this.getDragData(e));
}
}
}
},

onItemClick : function(item, index, e){
// The DragZone's mousedown->getDragData already handled selection
if (this.ignoreNextClick) {
delete this.ignoreNextClick;
return;
}

if(this.fireEvent("beforeclick", this, index, item, e) === false){
return false;
}
if(this.multiSelect || this.singleSelect){
if(this.multiSelect && e.shiftKey && this.lastSelection){
this.select(this.getNodes(this.indexOf(this.lastSelection), index), false);
} else if (this.isSelected(item) && e.ctrlKey) {
this.deselect(item);
}else{
this.deselect(item);
this.select(item, this.multiSelect && e.ctrlKey);
this.lastSelection = item;
}
e.preventDefault();
}
return true;
},

deselect : function(nodeInfo){
var node = this.getNode(nodeInfo);
if(node && this.isSelected(node)){
Ext.fly(node).removeClass(this.selectedClass);
this.selections.remove(node);
}
}
});

function initializePage() {
var collection=[
{'id':'1','thumbUrl':'ext/examples/tree/images/thumbs/dance_fever.jpg', copies:1, sizeCode:0},
{'id':'2','thumbUrl':'ext/examples/tree/images/thumbs/gangster_zack.jpg', copies:1, sizeCode:0},
{'id':'3','thumbUrl':'ext/examples/tree/images/thumbs/kids_hug2.jpg', copies:1, sizeCode:0},
{'id':'4','thumbUrl':'ext/examples/tree/images/thumbs/kids_hug.jpg', copies:1, sizeCode:0},
{'id':'5','thumbUrl':'ext/examples/tree/images/thumbs/sara_pink.jpg', copies:1, sizeCode:0},
{'id':'6','thumbUrl':'ext/examples/tree/images/thumbs/sara_pumpkin.jpg', copies:1, sizeCode:0},
{'id':'7','thumbUrl':'ext/examples/tree/images/thumbs/sara_smile.jpg', copies:1, sizeCode:0},
{'id':'8','thumbUrl':'ext/examples/tree/images/thumbs/up_to_something.jpg', copies:1, sizeCode:0},
{'id':'9','thumbUrl':'ext/examples/tree/images/thumbs/zack.jpg', copies:1, sizeCode:0},
{'id':'10','thumbUrl':'ext/examples/tree/images/thumbs/zack_dress.jpg', copies:1, sizeCode:0},
{'id':'11','thumbUrl':'ext/examples/tree/images/thumbs/zack_hat.jpg', copies:1, sizeCode:0},
{'id':'12','thumbUrl':'ext/examples/tree/images/thumbs/zack_sink.jpg', copies:1, sizeCode:0},
{'id':'13','thumbUrl':'ext/examples/tree/images/thumbs/zacks_grill.jpg', copies:1, sizeCode:0}
];
var Photograph = Ext.data.Record.create([
{name: 'id'},
{name: 'thumbUrl'},
{name: 'copies'},
{name: 'sizeCode'},
]);
var photoReader = new Ext.data.JsonReader({
root: 'collection',
id: 'id'
}, Photograph);
var ds = new Ext.data.Store({
proxy: new Ext.data.MemoryProxy({collection:collection}),
reader: photoReader
});
var view = new Ext.ux.DDView('left-view-container','<div id="available-photo-{id}" class="available-photo"><img align="top" src="{thumbUrl}"></div>',{
name: 'photoSet',
multiSelect: true,
copy: true,
dragGroup: 'availPhotosDDGroup',
jsonRoot: 'collection',
store: ds
});
ds.load();

ds = new Ext.data.Store({
proxy: new Ext.data.MemoryProxy({collection:[]}),
reader: photoReader
});
purchased = new Ext.ux.DDView('right-view-container',
'<div id="selected-photo-{id}" class="photo-for-printing"><img align="top" src="{thumbUrl}">' +
'<div class="x-form-item"><label style="width:45px">Copies:</label><select name="photo-{id}-copies"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option></select></div>' +
'<div class="x-form-item"><label style="width:45px">Size:</label><select name="photo-{id}-size"><option value="0">18 x 12</option><option value="1">10 x 8</option><option value="0">8 x 6</option></select></div>' +
'<div class="x-form-item"><label style="width:45px">Double:</label><input type="radio" name="photo-{id}-double" value="1">yes<br/><input type="radio" name="photo-{id}-double" value="0">no</div>' +
'</div>',
{
name: 'purchasedPrints',
dropGroup: 'availPhotosDDGroup',
jsonRoot: 'collection',
appendOnly: true,
deletable: "./ext/examples/shared/icons/fam/cross.gif",
store: ds
}
);
ds.load();

Ext.get("photo-selection").on("submit", function(e) {
e.stopEvent();
});
}
Ext.onReady(initializePage);
</script>
</head>
<body>
<p>
This demonstrates the drag/drop enabled Ext View. The View's template in the "for printing"
area includes input fields named "photo-" + the photograph's id + "-copies" and
"photo-" + the photograph's id + "-size" so the form is ready for submission to a server
with no complex processing.
<h1>Select Photographs for Printing</h1>
<form style="padding-top:20px;padding-left:10px" class="x-form x-box-blue" id="photo-selection">
<div class="x-form-ct x-form-column x-form-label-left">
<div class="x-box-tl"><div class="x-box-tr"><div class="x-box-tc"></div></div></div>
<div class="x-box-ml">
<div class="x-box-mr">
<div class="x-box-mc">
<h3>Your photographs</h3>
<div id="left-view-container" class="x-form-view" style="overflow:auto;height:420px"></div>
</div>
</div>
</div>
<div class="x-box-bl"><div class="x-box-br"><div class="x-box-bc"></div></div></div>
</div>
<div style="padding-left:10px" class="x-form-ct x-form-column x-form-label-left ">
<div class="x-box-tl"><div class="x-box-tr"><div class="x-box-tc"></div></div></div>
<div class="x-box-ml">
<div class="x-box-mr">
<div class="x-box-mc">
<h3>Selected for printing</h3>
<div id="right-view-container" class="x-form-view" style="overflow:auto;height:420px"></div>
</div>
</div>
</div>
<div class="x-box-bl"><div class="x-box-br"><div class="x-box-bc"></div></div></div>
</div>
<br>
<button type="submit">Submit</button>
<br/>
<br/>
<div class="x-form-item"><label style="width:45px">Double:</label><input type="radio" name="photo-xx-double" value="1">yes<br/><input type="radio" name="photo-xx-double" value="0">no</div>

</form>
</body>
</html>

Animal
12 Oct 2007, 9:50 PM
I see the problem. I think it could br a browser bug with <input type="radio"> definitions in innerHTML!

You might have to add some event jiggery-pokery to dynamically create these elements using DomHelper with useDom=true.

bowa
15 Oct 2007, 6:17 AM
using the example again.

it seems as the template that DDView expects as the second argument is not a Ext.Template

i tried this:


var tplHtml = '<div id="selected-photo-{id}" class="photo-for-printing"><img align="top" src="{thumbUrl}">' +
'<div class="x-form-item"><label style="width:45px">Copies:</label><select name="photo-{id}-copies"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option></select></div>' +
'<div class="x-form-item"><label style="width:45px">Size:</label><select name="photo-{id}-size"><option value="0">18 x 12</option><option value="1">10 x 8</option><option value="0">8 x 6</option></select></div>' +
'<div class="x-form-item"><label style="width:45px">Double:</label><input type="radio" name="photo-{id}-double" value="1">yes<br/><input type="radio" name="photo-{id}-double" value="0">no</div>' +
'</div>';

Ext.DomHelper.useDom = true;
var tpl = Ext.DomHelper.createTemplate(tplHtml);

purchased = new Ext.ux.DDView('right-view-container',tpl,
{

but firebug gives an error "tpl.replace is not a function", DDView treats it as a string, not as a Template.

So i don't really understand how i am supposed to use DomHelper here ...

Animal
15 Oct 2007, 8:39 AM
Are you using Ext 2.0?

Currently, the DDView has not been ported to Ext 2.0.

bowa
15 Oct 2007, 9:28 AM
no i am using 1.x

i take it that http://extjs.com/deploy/ext/docs/index.html still is about 1.x ?

Animal
15 Oct 2007, 10:54 AM
Yeah, Ext 2 docs are in deploy/dev/docs

bowa
15 Oct 2007, 11:25 AM
ok.

So any idea how i can create the template outside of DDView, while still being able to use injection of the record data ?

Or any other approach ?

Animal
15 Oct 2007, 11:11 PM
using the example again.

it seems as the template that DDView expects as the second argument is not a Ext.Template

i tried this:


var tplHtml = '<div id="selected-photo-{id}" class="photo-for-printing"><img align="top" src="{thumbUrl}">' +
'<div class="x-form-item"><label style="width:45px">Copies:</label><select name="photo-{id}-copies"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option></select></div>' +
'<div class="x-form-item"><label style="width:45px">Size:</label><select name="photo-{id}-size"><option value="0">18 x 12</option><option value="1">10 x 8</option><option value="0">8 x 6</option></select></div>' +
'<div class="x-form-item"><label style="width:45px">Double:</label><input type="radio" name="photo-{id}-double" value="1">yes<br/><input type="radio" name="photo-{id}-double" value="0">no</div>' +
'</div>';

Ext.DomHelper.useDom = true;
var tpl = Ext.DomHelper.createTemplate(tplHtml);

purchased = new Ext.ux.DDView('right-view-container',tpl,
{

but firebug gives an error "tpl.replace is not a function", DDView treats it as a string, not as a Template.

So i don't really understand how i am supposed to use DomHelper here ...

I don't think you should use DomHelper.createTemplate, just pass the string straight into the DDView constructor. That does type checking and will create the template for you.

bowa
15 Oct 2007, 11:45 PM
I see the problem. I think it could br a browser bug with <input type="radio"> definitions in innerHTML!

You might have to add some event jiggery-pokery to dynamically create these elements using DomHelper with useDom=true.

euhm ... totally confused now ...

bowa
16 Oct 2007, 3:43 AM
i commented out this part in DDView code, where it treats tpl as a string and no longer as Template as in Ext.View and now i can use (compiled) templates.

i don't really see why this code is there, those 'x-combo-list-item' classes don't get added to the code in the example either. perhaps some old code that hasn't been removed.


Ext.ux.DDView = function(container, tpl, config) {
/**
if (this.classRe.test(tpl)) {
tpl = tpl.replace(this.classRe, 'class=$1x-combo-list-item $2$1');
} else {
tpl = tpl.replace(this.tagRe, '$1 class="x-combo-list-item"');
}
**/

Animal
16 Oct 2007, 11:39 AM
The temnplate gets compiled eventually. I just accept string template defs so I can add the x-combo-list-item class to it so that they get the same UI as combo items.

bowa
16 Oct 2007, 1:25 PM
but in your picture order example they don't get added ?

vtswingkid
22 Oct 2007, 7:45 AM
DDView:

When copy is set to true. More than 1 than copy is ignored.

So drag and drop one element and it works fine a copy is made and the source is left alone.
Repeat the drop and the target does not add a second copy to the data store.

I would like to be able to drag 1 or more copies to the target.

vtswingkid
22 Oct 2007, 8:06 AM
I added an allowDup flag.
This will allow for multiple duplication of records, by requesting a new id for duplicates...

EDIT...done away with the diff...
here is the file.

alanwilliamson
24 Oct 2007, 8:44 AM
Animal this is indeed a thing of beauty.

I have hit into a wee problem; i am dragging between two DDView and when i drop i get:



r has no properties
[Break on this error] var dup = this.store.getById(r.id);

MD
6 Nov 2007, 12:05 AM
Hmmm, using the current code from page 1 with the example from page 3, anytime I shift- or ctrl- select (just selecting, not even dragging) more than one item in the right panel, it starts creating duplicates outside the fieldsets. Happens on IE6, 7 and in FF alarmingly without errors.

Edit: vtswingkid's version above works nicely. 21 diffs compared to the code in post #1. Animal, are you still keeping it current?

MD
7 Nov 2007, 3:35 AM
So I've played around a bit with this Extension (including the mods made to it by vtswingkid), and I must say what a greatly useful one it is and I'm surprised this isn't included as part of the core!

One thing I did notice however, is a flaw in the Ctrl-drag copying of nodes. In its current state, the script requires you to be holding down Ctrl while selecting the node(s), as opposed to a typical desktop paradigm of allowing that *plus* the alternative of allowing Ctrl to be pressed mid-drag, post-selection/pre-drop.

I've made a single-line modification to accomodate this, as well as fixing up ctrl-dblclick-copying of multiple nodes:


//if (data.copy) {
if(data.sourceView.copy || data.sourceView.allowCopy && e && e.ctrlKey || !e && data.copy){

I'm including below a full self-contained example (edit paths to reflect your ext location), though I've added in a few lines for config'ing the allowCopy/allowDupe, removed the node borders and forced the insertion indicator border CSS so that you see the little "nudge" when the indicator position moves:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<style>
* {
font-family:tahoma,arial,helvetica,sans-serif;
background-color:#EDF3FA;
}

.x-form-view {
cursor:default;
}

.Component {
background-color:lightblue;
cursor:pointer;
}

.asp-selected {
background-color:#000070 !important;
color:white;
}

.x-view-drag-insert-above {
border-top:1px dotted #3366cc !important;
}

.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc !important;
}

#left-view-container .x-combo-list-item, #right-view-container .x-combo-list-item {
border: 0;
}
</style>
<link rel="stylesheet" type="text/css" href="../../resources/scripts/ext/resources/css/ext-all.css" />
<script type="text/javascript" src="../../resources/scripts/ext/adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="../../resources/scripts/ext/ext-all.js"></script>
<script type="text/javascript">
Array.prototype.contains = function(element) {
for (var i = 0; i < this.length; i++) {
if (this[i] == element) {
return true;
}
}
return false;
};

Ext.namespace("Ext.ux");

/**
* @class Ext.ux.DDView
* A DnD enabled version of Ext.View.
* @param {Element/String} container The Element in which to create the View.
* @param {String} tpl The template string used to create the markup for each element of the View
* @param {Object} config The configuration properties. These include all the config options of
* {@link Ext.View} plus some specific to this class.<br>
* <p>
* Drag/drop is implemented by adding {@link Ext.data.Record}s to the target DDView. If copying is
* not being performed, the original {@link Ext.data.Record} is removed from the source DDView.<br>
* <p>
* The following extra CSS rules are needed to provide insertion point highlighting:<pre><code>
.x-view-drag-insert-above {
border-top:1px dotted #3366cc;
}
.x-view-drag-insert-below {
border-bottom:1px dotted #3366cc;
}
</code></pre>
*
*/
Ext.ux.DDView = function(container, tpl, config) {
if (this.classRe.test(tpl)) {
tpl = tpl.replace(this.classRe, 'class=$1x-combo-list-item $2$1');
} else {
tpl = tpl.replace(this.tagRe, '$1 class="x-combo-list-item" $2');
}

Ext.ux.DDView.superclass.constructor.apply(this, arguments);
this.getEl().setStyle("outline", "0px none");
this.getEl().unselectable();
if (this.dragGroup) {
this.setDraggable(this.dragGroup.split(","));
}
if (this.dropGroup) {
this.setDroppable(this.dropGroup.split(","));
}
if (this.deletable) {
this.setDeletable(this.deletable);
}
this.isDirtyFlag = false;
this.addEvents({
"drop" : true
});
};

Ext.extend(Ext.ux.DDView, Ext.View, {
/** @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone. */
/** @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone. */
/** @cfg {Boolean} copy Causes drag operations to copy nodes rather than move. (add this to the view that entires are to be copied from) */
/** @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move. (add this to the view that entries are to be copied from) */
/** @cfg {Boolean} allowDupe Causes ctrl/drag operations to copy nodes as a node with a new id if a previous copy already exists. (add this to the view that is to accept duplicate entries) */

isFormField: true,

classRe: /class=(['"])(.*)\1/,

tagRe: /(<\w*)(.*?>)/,

reset: Ext.emptyFn,

clearInvalid: Ext.form.Field.prototype.clearInvalid,

validate: function() {
return true;
},

destroy: function() {
this.purgeListeners();
this.getEl().removeAllListeners();
this.getEl().remove();
if (this.dragZone) {
if (this.dragZone.destroy) {
this.dragZone.destroy();
}
}
if (this.dropZone) {
if (this.dropZone.destroy) {
this.dropZone.destroy();
}
}
},

// Override: Don't select when input elements are clicked on
onClick: function(e) {
if (e.getTarget().tagName.match(/^(:?select|input)$/i)) {
e.stopEvent();
return;
}
Ext.ux.DDView.superclass.onClick.apply(this, arguments);
},

/** Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}. */
getName: function() {
return this.name;
},

/** Loads the View from a JSON string representing the Records to put into the Store. */
setValue: function(v) {
if (!this.store) {
throw "DDView.setValue(). DDView must be constructed with a valid Store";
}
var data = {};
data[this.store.reader.meta.root] = v ? [].concat(v) : [];
this.store.proxy = new Ext.data.MemoryProxy(data);
this.store.load();
},

/** @return {String} a parenthesised list of the ids of the Records in the View. */
getValue: function() {
var result = '(';
this.store.each(function(rec) {
result += rec.id + ',';
});
return result.substr(0, result.length - 1) + ')';
},

getIds: function() {
var i = 0, result = new Array(this.store.getCount());
this.store.each(function(rec) {
result[i++] = rec.id;
});
return result;
},

isDirty: function() {
return this.isDirtyFlag;
},

/**
* Part of the Ext.dd.DropZone interface. If no target node is found, the
* whole Element becomes the target, and this causes the drop gesture to append.
*/
getTargetFromEvent : function(e) {
var target = e.getTarget();
while ((target !== null) && (target.parentNode != this.el.dom)) {
target = target.parentNode;
}
if (!target) {
target = this.el.dom.lastChild || this.el.dom;
}
return target;
},

/**
* Create the drag data which consists of an object which has the property "ddel" as
* the drag proxy element.
*/
getDragData : function(e) {
var target = this.findItemFromChild(e.getTarget());
if(target) {
if (!this.isSelected(target)) {
delete this.ignoreNextClick;
this.onItemClick(target, this.indexOf(target), e);
this.ignoreNextClick = true;
}
var dragData = {
sourceView: this,
viewNodes: [],
records: [],
copy: this.copy || (this.allowCopy && e.ctrlKey)
};
if (this.getSelectionCount() == 1) {
var i = this.getSelectedIndexes()[0];
var n = this.getNode(i);
dragData.viewNodes.push(dragData.ddel = n);
dragData.records.push(this.store.getAt(i));
dragData.repairXY = Ext.fly(n).getXY();
} else {
if (this.dragZone.dragData.ddel) {
var d = Ext.get(this.dragZone.dragData.ddel);
if (d.hasClass('multi-proxy')) {
d.update('');
}
}
dragData.ddel = document.createElement('div');
dragData.ddel.className = 'multi-proxy';
this.collectSelection(dragData);
}
return dragData;
}
return false;
},

// override the default repairXY.
getRepairXY : function(e){
return this.dragData.repairXY;
},

/** Put the selections into the records and viewNodes Arrays. */
collectSelection: function(data) {
data.repairXY = Ext.fly(this.getSelectedNodes()[0]).getXY();
if (this.preserveSelectionOrder === true) {
Ext.each(this.getSelectedIndexes(), function(i) {
var n = this.getNode(i);
var dragNode = n.cloneNode(true);
dragNode.id = Ext.id();
data.ddel.appendChild(dragNode);
data.records.push(this.store.getAt(i));
data.viewNodes.push(n);
}, this);
} else {
var i = 0;
this.store.each(function(rec){
if (this.isSelected(i)) {
var n = this.getNode(i);
var dragNode = n.cloneNode(true);
dragNode.id = Ext.id();
data.ddel.appendChild(dragNode);
data.records.push(this.store.getAt(i));
data.viewNodes.push(n);
}
i++;
}, this);
}
},

/** Specify to which ddGroup items in this DDView may be dragged. */
setDraggable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDraggable, this);
return;
}
if (this.dragZone) {
this.dragZone.addToGroup(ddGroup);
} else {
this.dragZone = new Ext.dd.DragZone(this.getEl(), {
containerScroll: true,
ddGroup: ddGroup
});
// Draggability implies selection. DragZone's mousedown selects the element.
if (!this.multiSelect) { this.singleSelect = true; }

// Wire the DragZone's handlers up to methods in *this*
this.dragZone.getDragData = this.getDragData.createDelegate(this);
this.dragZone.getRepairXY = this.getRepairXY;
this.dragZone.onEndDrag = this.onEndDrag;
}
},

/** Specify from which ddGroup this DDView accepts drops. */
setDroppable: function(ddGroup) {
if (ddGroup instanceof Array) {
Ext.each(ddGroup, this.setDroppable, this);
return;
}
if (this.dropZone) {
this.dropZone.addToGroup(ddGroup);
} else {
this.dropZone = new Ext.dd.DropZone(this.getEl(), {
owningView: this,
containerScroll: true,
ddGroup: ddGroup
});

// Wire the DropZone's handlers up to methods in *this*
this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this);
this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this);
this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this);
this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this);
this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this);
}
},

/** Decide whether to drop above or below a View node. */
getDropPoint : function(e, n, dd){
if (n == this.el.dom) { return "above"; }
var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight;
var c = t + (b - t) / 2;
var y = Ext.lib.Event.getPageY(e);
if(y <= c) {
return "above";
}else{
return "below";
}
},

isValidDropPoint: function(pt, n, data) {
if (data.viewNodes.length > 1) {
return true;
}
var d = data.viewNodes[0];
if (d == n) {
return false;
}
if ((pt == "below") && (n.nextSibling == d)) {
return false;
}
if ((pt == "above") && (n.previousSibling == d)) {
return false;
}
return true;
},

onNodeEnter : function(n, dd, e, data){
if (this.highlightColor && (data.sourceView != this)) {
this.el.highlight(this.highlightColor);
}
return false;
},

onNodeOver : function(n, dd, e, data){
var dragElClass = this.dropNotAllowed;
var pt = this.getDropPoint(e, n, dd);
if (this.isValidDropPoint(pt, n, data)) {
if (this.appendOnly) {
return "x-tree-drop-ok-below";
}

// set the insert point style on the target node
if (pt) {
var targetElClass;
if (pt == "above"){
dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";
targetElClass = "x-view-drag-insert-above";
} else {
dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";
targetElClass = "x-view-drag-insert-below";
}
if (this.lastInsertClass != targetElClass){
Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass);
this.lastInsertClass = targetElClass;
}
}
}
return dragElClass;
},

onNodeOut : function(n, dd, e, data){
this.removeDropIndicators(n);
},

onNodeDrop : function(n, dd, e, data){
var pt = this.getDropPoint(e, n, dd);
var insertAt = (this.appendOnly || (n == this.el.dom)) ? this.nodes.length : n.nodeIndex;
if (pt == "below") {
insertAt++;
}

// Validate if dragging within a DDView
if (data.sourceView == this) {
// If the first element to be inserted below is the target node, remove it
if (pt == "below") {
if (data.viewNodes[0] == n) {
data.viewNodes.shift();
}
} else { // If the last element to be inserted above is the target node, remove it
if (data.viewNodes[data.viewNodes.length - 1] == n) {
data.viewNodes.pop();
}
}

// Nothing to drop...
if (!data.viewNodes.length) {
return false;
}

// If we are moving DOWN, then because a store.remove() takes place first,
// the insertAt must be decremented.
if (insertAt > this.store.indexOf(data.records[0])) {
insertAt--;
}
}

// Dragging from a Tree. Use the Tree's recordFromNode function.
if (data.node instanceof Ext.tree.TreeNode) {
var r = data.node.getOwnerTree().recordFromNode(data.node);
if (r) {
data.records = [ r ];
}
}

if (!data.records) {
alert("Programming problem. Drag data contained no Records");
return false;
}

for (var i = 0; i < data.records.length; i++) {
var r = data.records[i];
var dup = this.store.getById(r.id);
if (dup && (dd != this.dragZone)) {
if(!this.allowDupe){
Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1);
return true
}
var x=new Ext.data.Record();
r.id=x.id;
delete x;
}
if(data.sourceView.copy || data.sourceView.allowCopy && e && e.ctrlKey || !e && data.copy){
var c = r.copy();
if (this.fireEvent("drop", this, n, dd, e, data, c) !== false) {
this.store.insert(insertAt++, c);
}
} else {
if (this.fireEvent("drop", this, n, dd, e, data, r) !== false) {
if (data.sourceView) {
data.sourceView.isDirtyFlag = true;
data.sourceView.store.remove(r);
}
this.store.insert(insertAt++, r);
}
}
this.isDirtyFlag = true;
}
return true;
},

// Ensure the multi proxy is removed
onEndDrag: function(data, e) {
if (this.dragData.ddel) {
var d = Ext.get(this.dragData.ddel);
if (d.hasClass('multi-proxy')) {
d.remove();
//delete this.dragData.ddel;
}
}
},

removeDropIndicators : function(n){
if(n){
Ext.fly(n).removeClass([
"x-view-drag-insert-above",
"x-view-drag-insert-left",
"x-view-drag-insert-right",
"x-view-drag-insert-below"]);
this.lastInsertClass = "_noclass";
}
},

/**
* Utility method. Add a delete option to the DDView's context menu.
* @param {String} imageUrl The URL of the "delete" icon image.
*/
setDeletable: function(imageUrl) {
if (!this.singleSelect && !this.multiSelect) {
this.singleSelect = true;
}
var c = this.getContextMenu();
this.contextMenu.on("itemclick", function(item) {
switch (item.id) {
case "delete":
c.hide();
this.remove(this.getSelectedIndexes());
break;
}
}, this);
this.contextMenu.add({
icon: imageUrl,
id: "delete",
text: "Delete Item"
});
},

/** Return the context menu for this DDView. */
getContextMenu: function() {
if (!this.contextMenu) {
// Create the View's context menu
this.contextMenu = new Ext.menu.Menu({
id: this.id + "-contextmenu"
});
this.el.on("contextmenu", this.showContextMenu, this);
}
return this.contextMenu;
},

disableContextMenu: function() {
if (this.contextMenu) {
this.el.un("contextmenu", this.showContextMenu, this);
}
},

showContextMenu: function(e, item) {
item = this.findItemFromChild(e.getTarget());
if (item) {
e.stopEvent();
this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true);
this.contextMenu.showAt(e.getXY());
}
},

/**
* Remove {@link Ext.data.Record}s at the specified indices.
* @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove.
*/
remove: function(selectedIndices) {
selectedIndices = [].concat(selectedIndices);
for (var i = 0; i < selectedIndices.length; i++) {
var rec = this.store.getAt(selectedIndices[i]);
this.store.remove(rec);
}
},

/**
* Double click fires the event, but also, if this is draggable, and there is only one other
* related DropZone that is in another DDView, it drops the selected node on that DDView.
*/
onDblClick : function(e){
var item = this.findItemFromChild(e.getTarget());
if(item){
if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) {
return false;
}
if (this.dragGroup) {
var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true);

// Remove instances of this View's DropZone
while (targets.contains(this.dropZone)) {
targets.remove(this.dropZone);
}

// If there's only one other DropZone, and it is owned by a DDView, then drop it in
if ((targets.length == 1) && (targets[0].owningView)) {
this.dragZone.cachedTarget = null;
var el = Ext.get(targets[0].getEl());
var box = el.getBox(true);
targets[0].onNodeDrop(el.dom, {
target: el.dom,
xy: [box.x, box.y + box.height - 1]
}, null, this.getDragData(e));
}
}
}
},

onItemClick : function(item, index, e){
// The DragZone's mousedown->getDragData already handled selection
if (this.ignoreNextClick) {
delete this.ignoreNextClick;
return;
}

if(this.fireEvent("beforeclick", this, index, item, e) === false){
return false;
}
if(this.multiSelect || this.singleSelect){
if(this.multiSelect && e.shiftKey && this.lastSelection){
this.select(this.getNodes(this.indexOf(this.lastSelection), index), false);
} else if (this.isSelected(item) && e.ctrlKey) {
this.deselect(item);
}else{
this.deselect(item);
this.select(item, this.multiSelect && e.ctrlKey);
this.lastSelection = item;
}
e.preventDefault();
}
return true;
},

deselect : function(nodeInfo){
var node = this.getNode(nodeInfo);
if(node && this.isSelected(node)){
Ext.fly(node).removeClass(this.selectedClass);
this.selections.remove(node);
}
}
});

function initializePage() {
var collection=[];
var rec=Ext.data.Record.create([
{name:'id'},
{name:'entityImageUrl'},
{name:'componentDescription'}
]);
var reader=new Ext.data.JsonReader({
root:'collection',
id:'id'
},rec);
var ds=new Ext.data.Store({
proxy:new Ext.data.MemoryProxy({collection:collection}),
reader:reader
});
var view=new Ext.ux.DDView('left-view-container','<div id=\u0027subcomponent_{id}\u0027 class=\u0027Subcomponent\u0027><img align=\u0027top\u0027 height=\u002716px\u0027 width=\u002716px\u0027 src=\u0027{entityImageUrl}\u0027>{componentDescription}</div>',{
isFormField:true,
name:'subComponents',
//multiSelect: true,
allowDupe: true, //ENABLED TO DEMONSTRATE ALLOWING DUPLICATES
dragGroup:'availComponentDDGroup,subComponentDDGroup',
dropGroup:'availComponentDDGroup,subComponentDDGroup',
selectedClass: 'asp-selected',
jsonRoot: 'collection',
store: ds
});
ds.load();

collection=[
{'id':'40','entityImageUrl':'../../resources/gfx/famfamfam_silk_icons/icons/user_add.png','componentDescription':'Add User'},
{'id':'27','entityImageUrl':'../../resources/gfx/famfamfam_silk_icons/icons/user_delete.png','componentDescription':'Delete User'},
{'id':'28','entityImageUrl':'../../resources/gfx/famfamfam_silk_icons/icons/user_comment.png','componentDescription':'Comment on User'}
];
rec=Ext.data.Record.create([
{name:'id'},
{name:'entityImageUrl'},
{name:'componentDescription'}
]);
reader=new Ext.data.JsonReader({
root:'collection',
id:'id'
},rec);
ds=new Ext.data.Store({
proxy:new Ext.data.MemoryProxy({collection:collection}),
reader:reader
});
view=new Ext.ux.DDView('right-view-container','<div id=\u0027component_{id}\u0027 class=\u0027Component\u0027><img align=\u0027top\u0027 height=\u002716px\u0027 width=\u002716px\u0027 src=\u0027{entityImageUrl}\u0027>{componentDescription}</div>',{
isFormField:true,
name:'availableSubComponents',
multiSelect: true,
//copy: true,
allowCopy: true, //ENABLED TO DEMONSTRATE ALLOWING CTRL-DND/CTRL-DBLCLICK COPYING
dragGroup:'subComponentDDGroup',
dropGroup:'availComponentDDGroup',
selectedClass: 'asp-selected',
jsonRoot: 'collection',
store: ds
});
ds.load();
}
Ext.onReady(initializePage);
</script>
</head>
<body>
<form class="x-form" id="the-form">
<div class="x-form-ct x-form-column x-form-label-left">
<fieldset class="x-form-fieldset x-form-label-left" style="margin-left:10px" >
<legend>Subcomponents</legend>
<div id="left-view-container" class="x-form-view" style="overflow:auto;height:215px">
</div>
</fieldset>
</div>
<div class="x-form-ct x-form-column x-form-label-left ">
<fieldset class="x-form-fieldset x-form-label-left" style="margin-left:10px">
<legend>Available subcomponents</legend>
<div id="right-view-container" class="x-form-view" style="overflow:auto;height:215px">
</div>
</fieldset>
</div>
</form>
</body>
</html>

talshadar
7 Dec 2007, 11:06 AM
This control is exactly what I want - I'm trying to do something similar to BernardChhun's post on page 2. I have this DDView on the right that I populate with images gathered from a data store. I need to allow them to drag the images to a tree on the left that is basically a list of categories. The basic premise of this is the tree provides a selection of categories - selecting one will display the images in that category. I need to give them the ability to drag the images from the DDView to a different category (and thus changing it's category and removing it from the existing DDView) - I a still having issues with setting up paging properly. I'm including my code below but basically I can drag from the DDView but it won't let me drop on the Tree. I'm obviously missing something but can't track it down. Any help would be really appreciated - it's a great extension and if I could get it and the paging working it would go a long way to completing this project for me.



var categoryTreePanel = imageListLayout.add('west', new Ext.ContentPanel('categoriesDiv', {
title:'Image Categories',
fitToFrame:true,
autoScroll:true
}));

var treeEl = categoryTreePanel.getEl().createChild({tag:"div", id:"categoryFolders"});

var treeCategory = new Ext.tree.TreePanel(treeEl, {
animate:false,
loader: new Ext.tree.TreeLoader({
dataUrl:'BuildTree.aspx'
}),
enableDD:true,
dragGroup:'viewDragGroup', //as you can see I've tried alternatives...
dropGroup:'treeDropGroup',
ddGroup: 'treeDropGroup',
containerScroll: true
});

var root = new Ext.tree.AsyncTreeNode({
text: 'Image Control',
allowDrag:false,
allowDrop:false,
id:'100'
});

//var root = new Ext.tree.AsyncTreeNode();

treeCategory.setRootNode(root);
treeCategory.addListener("click", node_OnClick);

treeCategory.render();
root.expand();

// add an inline editor for the nodes
tCategoryEditor = new Ext.tree.TreeEditor(treeCategory, {
allowBlank:false,
blankText:'A name is required',
selectOnFocus:true
});

//add 2 listeners - 1 for finished edit and one before start to verify permisions
tCategoryEditor.addListener('beforestartedit', treeCategoryEdit_BeforeEdit);
tCategoryEditor.addListener('complete', treeCategoryEdit_Complete);

//add listeners for drag/drop
treeCategory.addListener('beforenodedrop',treeCategory_BeforeDrop);
treeCategory.addListener('nodedrop',treeCategory_AfterDrop);

//now load everything into main layout

layout = new Ext.BorderLayout(document.body, {
center: {
autoScroll:true,
tabPosition: "top",
fitToFrame:true,
autoCreate:true
}
}
);

// Build our content
imagesPanel = new Ext.NestedLayoutPanel(imageListLayout, {title: "Image List"} );

layout.beginUpdate();

layout.add("center", imagesPanel );

layout.endUpdate();

//had used this for the view but have to send in the string to the new DDView
tpl = new Ext.Template(
'<div class="thumb-wrap" id="{URL}">' +
'<div class="thumb"><img src="{URL}" class="thumb-img"></div>' +
'<span>{shortName}</span></div>'
);
tpl.compile();

var qtipTpl = new Ext.Template(
'<div class="image-tip"><img src="{URL}" align="left">'+
'<b>Image Name:</b>' +
'<span>{Title}</span>' +
'<b>Size:</b>' +
'<span>{sizeString}</span></div>'
);
qtipTpl.compile();


vwImageThmbs = new Ext.ux.DDView('imageThmbsDiv', '<div class="thumb-wrap" id="{URL}"><div class="thumb"><img src="{URL}" class="thumb-img"></div><span>{shortName}</span></div>',
{
totalPoperty : 'TotalCount',
fitToFrame: true,
multiSelect: true,
selectedClass: "selectedClass",
store: dsImageThumbs,
allowDup : false,
allowCopy : false,
dragGroup : 'viewDragGroup',
dropGroup : 'treeDropGroup'
}
);

vwImageThmbs.prepareData = function(data){
data.shortName = data.Title.ellipse(15);;
data.sizeString = (Math.round(((data.Size*10) / 1024))/10) + " KB";
//data.dateString = new Date(data.lastmod).format("m/d/Y g:i a");
data.qtip = new String(qtipTpl.applyTemplate(data));
// lookup[data.name] = data;
return data;
};

thumbsPagingToolbar = new Ext.PagingToolbar(viewThumbsFooter, dsImageThumbs, {
pageSize: "30",
displayInfo: true,
displayMsg: 'Rows {0} - {1} of {2}',
emptyMsg: "No records to display"
});

talshadar
11 Dec 2007, 9:42 AM
I've resolved my drag and drop issues so can now drag my images from the DDView over to a tree. Paging is still not working properly but I will come back to that.

talshadar
14 Dec 2007, 6:48 AM
I've taken examples from this thread to build and manipulate the view and for the most part everything is working pretty much the way I want. But I can't get the paging toolbar to behave properly. I'm including the code that I'm using to build the layout, the grid, tree and views. I'm really hoping someone can point me in the right direction. I need to get this completed as well as an image upload tab done by friday of next week. It has to get done in this year. Please Help!!




// Layout my grid
var imageColModel = new Ext.grid.ColumnModel([
{id:"Title", header: "Title", dataIndex:"Title", width: 150,sortable: true},
{id:"Filename", header: "Filename", dataIndex:"Filename", sortable: true},
{id:"Category", header: "Category", dataIndex:"Category", width: 30, sortable: true},
{id:"Originator", header: "Originator", dataIndex:"Originator", width: 40, sortable: true},
{id:"PhotoDate", header: "PhotoDate", dataIndex:"PhotoDate", width: 75, sortable: true},
{id:"Description", header: "Description", dataIndex:"Description", width: 170, sortable: true}
]);

// set the grid to select only a single row at a time and to listen for rowselect events
var imageRowSel = new Ext.grid.RowSelectionModel({
singleSelect:false
});

// Define the search grid
imagesGrid = new Ext.grid.Grid("imagesDiv",{
ds:dsImages,
autoExpandColumn:"Filename",
cm:imageColModel,
loadMask: {msg: "Loading Images..."},
selModel:imageRowSel,
enableColLock: true,
autoCreate: true,
stripeRows: true
});

imagesGrid.addListener("rowdblclick", imagesGrid_OnRowDblClick);

Tree = Ext.tree;

imageListLayout = new Ext.BorderLayout('imageListLayout', {
west: {
initialSize:200,
split:true,
autoCreate:true,
fitToFrame:true,
titlebar:true,
autoScroll:true
},
center: {
initialSize:600,
titlebar:true,
autoCreate:true,
fitToFrame:true,
autoScroll:true
}
});

imagesList = imageListLayout.add("center", new Ext.GridPanel(imagesGrid, {
title:"Image List",
initialSize:600,
autoCreate:true,
fitToFrame:true,
autoScroll:true
}) );

imagesThmbs = imageListLayout.add('center', new Ext.ContentPanel('imageThmbsDiv', {
title:'Image Thumbnails',
initialSize:600,
fitToFrame:false,
autoScroll:true,
autoCreate:true
}));

imgBody = imagesThmbs.getEl();

imagesGrid.render();

var gridFoot = imagesGrid.getView().getFooterPanel(true);

// add a paging toolbar to the grid's footer
var paging = new Ext.PagingToolbar(gridFoot, dsImages, {
pageSize: 20,
displayInfo: true,
displayMsg: 'Rows {0} - {1} of {2}',
emptyMsg: "No records to display"
});

//var categoryTreePanel = imageListLayout.add('west', new Ext.ContentPanel(albums, {
categoryTreePanel = imageListLayout.add('west', new Ext.ContentPanel('categoriesDiv', {
title:'Image Categories',
fitToFrame:true,
autoScroll:true
}));

treeEl = categoryTreePanel.getEl().createChild({tag:"div", id:"categoryFolders"});

treeCategory = new Ext.tree.TreePanel(treeEl, {
// var tree = new Ext.tree.TreePanel('catTreeDiv', {
animate:false,
loader: new Ext.tree.TreeLoader({
dataUrl:'BuildTree.aspx'
}),
enableDD:true,
ddGroup: 'treeDropGroup',
containerScroll: true
});

//add listener for right click context menu
//categoryTreePanel.addListener('contextmenu',treeCategoryContextMenu_onRightClick);
treeCategory.addListener('contextmenu',treeCategoryContextMenu_onRightClick);

// set the root node

treeRoot = new Ext.tree.AsyncTreeNode({
text: 'Image Categories',
allowDrag:false,
allowDrop:false,
id:'0000'
});

//var root = new Ext.tree.AsyncTreeNode();

treeCategory.setRootNode(treeRoot);
treeCategory.addListener("click", node_OnClick);

// render the tree
treeCategory.render();
treeRoot.expand();

// add an inline editor for the nodes
tCategoryEditor = new Ext.tree.TreeEditor(treeCategory, {
allowBlank:false,
blankText:'A name is required',
selectOnFocus:true
});

//add 2 listeners - 1 for finished edit and one before start to verify permisions
tCategoryEditor.addListener('beforestartedit', treeCategoryEdit_BeforeEdit);
tCategoryEditor.addListener('complete', treeCategoryEdit_Complete);

//add listeners for drag/drop
treeCategory.addListener('beforenodedrop',treeCategory_BeforeDrop);
treeCategory.addListener('nodedrop',treeCategory_AfterDrop);

//now load everything into main layout

layout = new Ext.BorderLayout(document.body, {
center: {
autoScroll:true,
tabPosition: "top",
fitToFrame:true,
autoCreate:true
}
}
);

// Build our content
imagesPanel = new Ext.NestedLayoutPanel(imageListLayout, {title: "Image List"} );

layout.beginUpdate();

layout.add("center", imagesPanel );

layout.endUpdate();

layout.getRegion("center").showPanel("Image List");
///////////////////////////

// now setup the templates etc required to show thumbnails of the images
// create the required templates

tpl = new Ext.Template(
'<div class="thumb-wrap" >' +
'<div class="thumb"><img src="{URL}" class="thumb-img"></div>' +
'<span>{shortName}</span></div>'
);
tpl.compile();

var qtipTpl = new Ext.Template(
'<div id="{URL}" class="image-tip"><img src="{URL}" align="left">'+
'<b>Image Name:</b>' +
'<span>{Title}</span>' +
'<b>Size:</b>' +
'<span>{sizeString}</span></div>'
);
qtipTpl.compile();

// vwImageThmbs = new Ext.ux.DDView('imageThmbsDiv', '<div class="thumb-wrap"><div class="thumb"><img src="{URL}" class="thumb-img"></div><span>{shortName}</span></div>',
vwImageThmbs = new Ext.ux.DDView(imgBody, '<div id="{imageLink}" class="thumb-wrap"><div class="thumb"><img src="{URL}" class="thumb-img"></div><span>{shortName}</span></div>',
{
animate: true,
totalPoperty : 'TotalCount',
fitToFrame: true,
multiSelect: true,
selectedClass: "selectedClass",
store: dsImageThumbs,
allowDup : false,
allowCopy : false,
dragGroup : 'treeDropGroup',
dropGroup : 'viewDragGroup'
}
);

// viewThumbsFooter = Ext.get(imagesThmbs.el.dom.parentNode).createChild({tag: "div", cls:"viewFooter"});
viewThumbsFooter = Ext.get(vwImageThmbs.el.dom.parentNode).createChild({tag: "div", cls:"viewFooter"});

vwImageThmbs.prepareData = function(data){
data.shortName = data.Title.ellipse(15);;
data.sizeString = (Math.round(((data.Size*10) / 1024))/10) + " KB";
//data.dateString = new Date(data.lastmod).format("m/d/Y g:i a");
data.qtip = new String(qtipTpl.applyTemplate(data));
return data;
};

thumbsPagingToolbar = new Ext.PagingToolbar(viewThumbsFooter, dsImageThumbs, {
pageSize: "30",
displayInfo: true,
displayMsg: 'Rows {0} - {1} of {2}',
emptyMsg: "No records to display"
});

talshadar
17 Dec 2007, 11:46 AM
Everything seems to be working ok. There's some issues with how the images are laying in the DDView but that's workable. But I can not get paging to work regardless of what I do. Has anyone successfully got paging to work with the DDView?

As I've mentioned above the first page works but as soon as I hit Next Page - it displays 101 of 8 pages and displays all 230+ images.

I'm using a MemoryProxy to grab all the records out of the database instead of just 30 at a time. I'm successfully using this method with other grids - it's just the DDView that is not working properly.

I've done everything I can think - I've added an additional borderlayout and panel in order to get the paging toolbar to appear properly (it wanted to tie to the parent of the view which was my initial border layout. So that's resolved - but the paging just isn't working.

I haven't reposted to code again because it is in the post above - the paging and view peice hasn't changed at all.

Please help!!!

talshadar
18 Dec 2007, 9:31 AM
Ok, after hours of banging my head against the wall (with no help) I've come to the conclusion that the DDView will not work with the MemoryPagingProxy http://extjs.com/forum/showthread.php?t=11652.

This is more than a little inconvenient. So I either have to change how I'm handling data (creating a set of DDView specific queries to use limits etc) so that I can still utilize the DDView as well as a paging toolbar .. or toss the paging toolbar and load all the images for each query - potential several hundred.

Neither option appeals to me in the least - to me, both break the target functionality that I require for this application.

Can ANYONE give me a suggestion or point me in a better direction then these 2 options??

talshadar
21 Dec 2007, 10:20 AM
I've resolved the paging error - but I'm still having a layout issue with the placement of the paging toolbar.

Regarding the paging error. It was how I was declaring the value for the records per page:

This is what I had:


thumbsPagingToolbar = new Ext.PagingToolbar(viewThumbsFooter, dsImageThumbs, {
pageSize: "30",
displayInfo: true,
displayMsg: 'Rows {0} - {1} of {2}',
emptyMsg: "No records to display"
});


I removed the quotes around the pageSize:


thumbsPagingToolbar = new Ext.PagingToolbar(viewThumbsFooter, dsImageThumbs, {
pageSize: 30,
displayInfo: true,
displayMsg: 'Rows {0} - {1} of {2}',
emptyMsg: "No records to display"
});


And boom it works!!! So my holiday will be starting off in a positive note!

GraemeBryce
4 Jan 2008, 4:00 AM
Animal

In your code the lines



// Dragging from a Tree. Use the Tree's recordFromNode function.
if (data.node instanceof Ext.tree.TreeNode) {
var r = data.node.getOwnerTree().recordFromNode(data.node);
if (r) {
data.records = [ r ];
}
}



make a call to recordFromNode(data.node)

I am trying to use the object but it fails on this line and I am unable to work out from where the recordFromNode method is being called. This is not a documented method of a TreePanel ??

Is this a bug (I confess confidence that it is not) Or what have I failed to grasp.

Any help much appreciated.
Regards: Graeme

GraemeBryce
4 Jan 2008, 4:59 AM
oops

I see that you explain this on page 5 of this thread.

Can I also comment that if future versions of the tree could support a strore and records then as you have already pointed out, all of this and many items to do with loading trees would become easier.

That said, its not quite that easy as the tree is able to load nested data with different attributes at each level - at present a store is a flat concept.

What is perhaps needed is the concept of a store within a store ?

Thanks for this significant extension.
Graeme

knight9
25 Jan 2008, 8:31 AM
Are there any plans to extend this for Ext 2.0? I would love to use this extension, but am in a 2.0 environment?

GraemeBryce
25 Jan 2008, 9:09 AM
The code on page 1 of this thread does run with EXT 2.0

pomata
28 Jan 2008, 4:19 PM
do we have a working example?

doesn

knight9
29 Jan 2008, 10:03 AM
I get the same error

vtswingkid
6 Feb 2008, 7:52 AM
there is a 2.0 version in the multiselector extension.... :)

http://extjs.com/learn/Extension:Multiselect2

talshadar
21 Feb 2008, 8:38 AM
I've been looking at the possibility of using a LoadMask with this extension but so far I'm having no luck at all.

Because I can't get the paging to work properly I'm loading all the images into the view - this can be quite a few images but that's how the boss wants it - I would llike to be able to use a mask of some type to let them know that images are loading etc. Something to not only let them know what is happening but to also stop them from getting click-happy and clicking everything they can find.

I don't even know if it's possible and my first initial attempts haven't worked so I thought I'd ask before I spent a lot of time on it.

NOTE: I'm using the 1.1 version of the DDView.

talshadar
21 Feb 2008, 10:26 AM
I need to be able to catch the selections from the context menu. I figured out how to turn on the "delete" for the right-click context menu but I need it to run a particular function etc. Currently, from what I understand, it simply removes it from the data-source. That's fine I guess - but it doesn't do any verification or anything. I need to be able to catch it so that:

a) I can verify that they really want to delete the item
and
b)fire a function that will actually delete that item as required.

I have absolutely NO idea how to do this short of re-writing the entire extension which I really don't want to do. Does anyone have an idea??

This also leads to another question - what if I wanted to use my own customized menu - instead of just delete (with a default text of Delete Item) - but custom options to do things i specify?? This seems, to me, to be something that would be a nice almost necessary option??

********************************
Made some progress but things still aren't working properly. I've setup a copy of an existing Context Menu I set up for a grid (that works great by the way) but it doesn't behave correctly for this extension. I shut off the deleteable option and created a listener specific to the DDView. I have a function that builds the context menu - but it's not working with what ever object that gets returned from the DDView. I'll include the code examples below - what I need is detail/information on what type of object gets returned and how I can interact with it because nothing I do is working.

I REALLY need help on this.



vwImageThmbs.addListener('contextmenu',vwImageContextMenu_onRightClick);

function vwImageContextMenu_onRightClick(node, e)
{
onContextMenuView(node,e);
}

function onContextMenuView(node, e)
{
this.contextmenu = createContextMenuView(node);
this.contextmenu.on('itemclick', onContextMenuSelectView, this);
this.contextmenu.showAt(e.xy);
}

function createContextMenuView(node)
{
var contextmenu = new Ext.menu.Menu({
id:node.PrimKey+'contextmenu'
, items: [
new Ext.menu.Item({
id:'rm-delete'
, icon: "images/delete.gif"
, text: "Delete" + node.shortName + "."
, nodeID: node.PrimKey
, nodeTitle: node.Title

})
]
});
return contextmenu;
}

function onContextMenuSelectView(item)
{
catNode = item.PrimKey;
catTitle = item.Title;
switch(item.id) {
case 'rm-delete':
if (catNode != "0000")
{
Ext.Msg.confirm("Confirm Delete", "Are you sure you wish to delete " + catTitle + '?', deleteCategory );
}
break;
default:
Ext.MessageBox.alert("onContextMenuSelect", "You've selected: " + catTitle );
break;
}
}

Emblem Parade
21 Jun 2008, 12:06 AM
I've been using the Ext 2 version of DDView included in the Multiselect extension with some success. I recommend one fix, which may also apply to the Ext 1 version.

The problem is in the destroy function: I have an application which changes CSS and other things on the fly, and the current DDView fails on destroy, because the element is somehow removed already. Here's my workaround, which may be imperfect. I added an if to make the the element is there:


destroy: function() {
this.purgeListeners();
if (this.getEl()) {
this.getEl().removeAllListeners();
this.getEl().remove();
}
if (this.dragZone) {
if (this.dragZone.destroy) {
this.dragZone.destroy();
}
}
if (this.dropZone) {
if (this.dropZone.destroy) {
this.dropZone.destroy();
}
}
},

t800t8
26 Oct 2009, 6:25 PM
DDView is a very nice controls.

t800t8
29 Oct 2009, 12:00 AM
I added a 'drop' event listener but when I call view.getIds() in the listner, it returns all ids with the order is same as before drag&drop. I expect there is a 'afterdrop" event so I can retrieve new list of ids but seems it isn't.

So how to retrieve list of ids with new order after drag&drop an item?

t800t8
29 Oct 2009, 5:54 PM
Just added 'afterdrop' event manually in onNodeDrop()