1. #1
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,499
    Vote Rating
    47
    Animal has a spectacular aura about Animal has a spectacular aura about

      0  

    Default A DnD enabled Ext.View

    A DnD enabled Ext.View


    Here's my current working version with API docs:

    Code:
    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.

  2. #2
    Ext User DigitalSkyline's Avatar
    Join Date
    Apr 2007
    Location
    Rochester, MI
    Posts
    461
    Vote Rating
    1
    DigitalSkyline is on a distinguished road

      0  

    Default


    Sounds great... any chance of a live demo

  3. #3
    Ext JS Premium Member
    Join Date
    Mar 2007
    Location
    germany
    Posts
    52
    Vote Rating
    0
    q_no is on a distinguished road

      0  

    Default


    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!

  4. #4
    Ext User
    Join Date
    Mar 2007
    Location
    Boston
    Posts
    349
    Vote Rating
    0
    sjivan is on a distinguished road

      0  

    Default


    Very handy. Hope it makes the Ext core someday.

    Sanjiv

  5. #5
    Sencha User
    Join Date
    Mar 2007
    Posts
    761
    Vote Rating
    1
    franklt69 is on a distinguished road

      0  

    Default


    some screenshot?


    regards
    Frank

  6. #6
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,499
    Vote Rating
    47
    Animal has a spectacular aura about Animal has a spectacular aura about

      0  

    Default


    Quote Originally Posted by q_no View Post
    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!
    I'm glad somebody could be bothered to try it out!

    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.

  7. #7
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,499
    Vote Rating
    47
    Animal has a spectacular aura about Animal has a spectacular aura about

      0  

    Default




    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.

  8. #8
    Ext JS Premium Member
    Join Date
    Mar 2007
    Location
    germany
    Posts
    52
    Vote Rating
    0
    q_no is on a distinguished road

      0  

    Default


    really nice

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

  9. #9
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,499
    Vote Rating
    47
    Animal has a spectacular aura about Animal has a spectacular aura about

      0  

    Default


    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.

  10. #10
    Ext JS Premium Member
    Join Date
    Mar 2007
    Location
    germany
    Posts
    52
    Vote Rating
    0
    q_no is on a distinguished road

      0  

    Default


    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?

Turkiyenin en sevilen filmlerinin yer aldigi xnxx internet sitemiz olan ve porn sex tarzi bir site olan mobil porno izle sitemiz gercekten dillere destan bir durumda herkesin sevdigi bir site olarak tarihe gececege benziyor. Sitenin en belirgin ozelliklerinden birisi de Turkiyede gercekten kaliteli ve muntazam, duzenli porno izle siteleri olmamasidir. Bu yuzden iste. Ayrica en net goruntu kalitesine sahip adresinde yayinlanmaktadir. Mesela diğer sitelerimizden bahsedecek olursak, en iyi hd porno video arşivine sahip bir siteyiz. "The Best anal porn videos and slut anus, big asses movies set..." hd porno faketaxi