2 * Ext JS Library 2.2.1
\r
3 * Copyright(c) 2006-2009, Ext JS, LLC.
\r
4 * licensing@extjs.com
\r
6 * http://extjs.com/license
\r
10 * Software License Agreement (BSD License)
\r
11 * Copyright (c) 2008, Nige "Animal" White
\r
12 * All rights reserved.
\r
14 * Redistribution and use in source and binary forms, with or without modification,
\r
15 * are permitted provided that the following conditions are met:
\r
17 * * Redistributions of source code must retain the above copyright notice,
\r
18 * this list of conditions and the following disclaimer.
\r
19 * * Redistributions in binary form must reproduce the above copyright notice,
\r
20 * this list of conditions and the following disclaimer in the documentation
\r
21 * and/or other materials provided with the distribution.
\r
22 * * Neither the name of the original author nor the names of its contributors
\r
23 * may be used to endorse or promote products derived from this software
\r
24 * without specific prior written permission.
\r
26 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
\r
27 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
\r
28 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
\r
29 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
\r
30 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
\r
31 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
\r
32 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
\r
33 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
\r
34 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
\r
35 * POSSIBILITY OF SUCH DAMAGE.
\r
38 * @class Ext.ux.DDView
\r
39 * <p>A DnD-enabled version of {@link Ext.DataView}. Drag/drop is implemented by adding
\r
40 * {@link Ext.data.Record}s to the target DDView. If copying is not being performed,
\r
41 * the original {@link Ext.data.Record} is removed from the source DDView.</p>
\r
43 * Create a new DDView
\r
44 * @param {Object} config The configuration properties.
\r
46 Ext.ux.DDView = function(config) {
\r
47 if (!config.itemSelector) {
\r
48 var tpl = config.tpl;
\r
49 if (this.classRe.test(tpl)) {
\r
50 config.tpl = tpl.replace(this.classRe, 'class=$1x-combo-list-item $2$1');
\r
53 config.tpl = tpl.replace(this.tagRe, '$1 class="x-combo-list-item" $2');
\r
55 config.itemSelector = ".x-combo-list-item";
\r
57 Ext.ux.DDView.superclass.constructor.call(this, Ext.apply(config, {
\r
62 Ext.extend(Ext.ux.DDView, Ext.DataView, {
\r
64 * @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone (defaults to undefined).
\r
67 * @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone (defaults to undefined).
\r
70 * @cfg {Boolean} copy Causes drag operations to copy nodes rather than move (defaults to false).
\r
73 * @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move (defaults to false).
\r
76 * @cfg {String} sortDir Sort direction for the view, 'ASC' or 'DESC' (defaults to 'ASC').
\r
82 classRe: /class=(['"])(.*)\1/,
\r
83 tagRe: /(<\w*)(.*?>)/,
\r
85 clearInvalid: Ext.form.Field.prototype.clearInvalid,
\r
88 afterRender: function() {
\r
89 Ext.ux.DDView.superclass.afterRender.call(this);
\r
90 if (this.dragGroup) {
\r
91 this.setDraggable(this.dragGroup.split(","));
\r
93 if (this.dropGroup) {
\r
94 this.setDroppable(this.dropGroup.split(","));
\r
96 if (this.deletable) {
\r
97 this.setDeletable();
\r
99 this.isDirtyFlag = false;
\r
106 validate: function() {
\r
111 destroy: function() {
\r
112 this.purgeListeners();
\r
113 this.getEl().removeAllListeners();
\r
114 this.getEl().remove();
\r
115 if (this.dragZone) {
\r
116 if (this.dragZone.destroy) {
\r
117 this.dragZone.destroy();
\r
120 if (this.dropZone) {
\r
121 if (this.dropZone.destroy) {
\r
122 this.dropZone.destroy();
\r
128 * Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}.
\r
130 getName: function() {
\r
135 * Loads the View from a JSON string representing the Records to put into the Store.
\r
136 * @param {String} value The JSON string
\r
138 setValue: function(v) {
\r
140 throw "DDView.setValue(). DDView must be constructed with a valid Store";
\r
143 data[this.store.reader.meta.root] = v ? [].concat(v) : [];
\r
144 this.store.proxy = new Ext.data.MemoryProxy(data);
\r
149 * Returns the view's data value as a list of ids.
\r
150 * @return {String} A parenthesised list of the ids of the Records in the View, e.g. (1,3,8).
\r
152 getValue: function() {
\r
154 this.store.each(function(rec) {
\r
155 result += rec.id + ',';
\r
157 return result.substr(0, result.length - 1) + ')';
\r
160 getIds: function() {
\r
161 var i = 0, result = new Array(this.store.getCount());
\r
162 this.store.each(function(rec) {
\r
163 result[i++] = rec.id;
\r
169 * Returns true if the view's data has changed, else false.
\r
170 * @return {Boolean}
\r
172 isDirty: function() {
\r
173 return this.isDirtyFlag;
\r
177 * Part of the Ext.dd.DropZone interface. If no target node is found, the
\r
178 * whole Element becomes the target, and this causes the drop gesture to append.
\r
180 getTargetFromEvent : function(e) {
\r
181 var target = e.getTarget();
\r
182 while ((target !== null) && (target.parentNode != this.el.dom)) {
\r
183 target = target.parentNode;
\r
186 target = this.el.dom.lastChild || this.el.dom;
\r
192 * Create the drag data which consists of an object which has the property "ddel" as
\r
193 * the drag proxy element.
\r
195 getDragData : function(e) {
\r
196 var target = this.findItemFromChild(e.getTarget());
\r
198 if (!this.isSelected(target)) {
\r
199 delete this.ignoreNextClick;
\r
200 this.onItemClick(target, this.indexOf(target), e);
\r
201 this.ignoreNextClick = true;
\r
207 copy: this.copy || (this.allowCopy && e.ctrlKey)
\r
209 if (this.getSelectionCount() == 1) {
\r
210 var i = this.getSelectedIndexes()[0];
\r
211 var n = this.getNode(i);
\r
212 dragData.viewNodes.push(dragData.ddel = n);
\r
213 dragData.records.push(this.store.getAt(i));
\r
214 dragData.repairXY = Ext.fly(n).getXY();
\r
216 dragData.ddel = document.createElement('div');
\r
217 dragData.ddel.className = 'multi-proxy';
\r
218 this.collectSelection(dragData);
\r
225 // override the default repairXY.
\r
226 getRepairXY : function(e){
\r
227 return this.dragData.repairXY;
\r
231 collectSelection: function(data) {
\r
232 data.repairXY = Ext.fly(this.getSelectedNodes()[0]).getXY();
\r
233 if (this.preserveSelectionOrder === true) {
\r
234 Ext.each(this.getSelectedIndexes(), function(i) {
\r
235 var n = this.getNode(i);
\r
236 var dragNode = n.cloneNode(true);
\r
237 dragNode.id = Ext.id();
\r
238 data.ddel.appendChild(dragNode);
\r
239 data.records.push(this.store.getAt(i));
\r
240 data.viewNodes.push(n);
\r
244 this.store.each(function(rec){
\r
245 if (this.isSelected(i)) {
\r
246 var n = this.getNode(i);
\r
247 var dragNode = n.cloneNode(true);
\r
248 dragNode.id = Ext.id();
\r
249 data.ddel.appendChild(dragNode);
\r
250 data.records.push(this.store.getAt(i));
\r
251 data.viewNodes.push(n);
\r
259 * Specify to which ddGroup items in this DDView may be dragged.
\r
260 * @param {String} ddGroup The DD group name to assign this view to.
\r
262 setDraggable: function(ddGroup) {
\r
263 if (ddGroup instanceof Array) {
\r
264 Ext.each(ddGroup, this.setDraggable, this);
\r
267 if (this.dragZone) {
\r
268 this.dragZone.addToGroup(ddGroup);
\r
270 this.dragZone = new Ext.dd.DragZone(this.getEl(), {
\r
271 containerScroll: true,
\r
274 // Draggability implies selection. DragZone's mousedown selects the element.
\r
275 if (!this.multiSelect) { this.singleSelect = true; }
\r
277 // Wire the DragZone's handlers up to methods in *this*
\r
278 this.dragZone.getDragData = this.getDragData.createDelegate(this);
\r
279 this.dragZone.getRepairXY = this.getRepairXY;
\r
280 this.dragZone.onEndDrag = this.onEndDrag;
\r
285 * Specify from which ddGroup this DDView accepts drops.
\r
286 * @param {String} ddGroup The DD group name from which to accept drops.
\r
288 setDroppable: function(ddGroup) {
\r
289 if (ddGroup instanceof Array) {
\r
290 Ext.each(ddGroup, this.setDroppable, this);
\r
293 if (this.dropZone) {
\r
294 this.dropZone.addToGroup(ddGroup);
\r
296 this.dropZone = new Ext.dd.DropZone(this.getEl(), {
\r
298 containerScroll: true,
\r
302 // Wire the DropZone's handlers up to methods in *this*
\r
303 this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this);
\r
304 this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this);
\r
305 this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this);
\r
306 this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this);
\r
307 this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this);
\r
312 getDropPoint : function(e, n, dd){
\r
313 if (n == this.el.dom) { return "above"; }
\r
314 var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight;
\r
315 var c = t + (b - t) / 2;
\r
316 var y = Ext.lib.Event.getPageY(e);
\r
325 isValidDropPoint: function(pt, n, data) {
\r
326 if (!data.viewNodes || (data.viewNodes.length != 1)) {
\r
329 var d = data.viewNodes[0];
\r
333 if ((pt == "below") && (n.nextSibling == d)) {
\r
336 if ((pt == "above") && (n.previousSibling == d)) {
\r
343 onNodeEnter : function(n, dd, e, data){
\r
344 if (this.highlightColor && (data.sourceView != this)) {
\r
345 this.el.highlight(this.highlightColor);
\r
351 onNodeOver : function(n, dd, e, data){
\r
352 var dragElClass = this.dropNotAllowed;
\r
353 var pt = this.getDropPoint(e, n, dd);
\r
354 if (this.isValidDropPoint(pt, n, data)) {
\r
355 if (this.appendOnly || this.sortField) {
\r
356 return "x-tree-drop-ok-below";
\r
359 // set the insert point style on the target node
\r
362 if (pt == "above"){
\r
363 dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";
\r
364 targetElClass = "x-view-drag-insert-above";
\r
366 dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";
\r
367 targetElClass = "x-view-drag-insert-below";
\r
369 if (this.lastInsertClass != targetElClass){
\r
370 Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass);
\r
371 this.lastInsertClass = targetElClass;
\r
375 return dragElClass;
\r
379 onNodeOut : function(n, dd, e, data){
\r
380 this.removeDropIndicators(n);
\r
384 onNodeDrop : function(n, dd, e, data){
\r
385 if (this.fireEvent("drop", this, n, dd, e, data) === false) {
\r
388 var pt = this.getDropPoint(e, n, dd);
\r
389 var insertAt = (this.appendOnly || (n == this.el.dom)) ? this.store.getCount() : n.viewIndex;
\r
390 if (pt == "below") {
\r
394 // Validate if dragging within a DDView
\r
395 if (data.sourceView == this) {
\r
396 // If the first element to be inserted below is the target node, remove it
\r
397 if (pt == "below") {
\r
398 if (data.viewNodes[0] == n) {
\r
399 data.viewNodes.shift();
\r
401 } else { // If the last element to be inserted above is the target node, remove it
\r
402 if (data.viewNodes[data.viewNodes.length - 1] == n) {
\r
403 data.viewNodes.pop();
\r
407 // Nothing to drop...
\r
408 if (!data.viewNodes.length) {
\r
412 // If we are moving DOWN, then because a store.remove() takes place first,
\r
413 // the insertAt must be decremented.
\r
414 if (insertAt > this.store.indexOf(data.records[0])) {
\r
419 // Dragging from a Tree. Use the Tree's recordFromNode function.
\r
420 if (data.node instanceof Ext.tree.TreeNode) {
\r
421 var r = data.node.getOwnerTree().recordFromNode(data.node);
\r
423 data.records = [ r ];
\r
427 if (!data.records) {
\r
428 alert("Programming problem. Drag data contained no Records");
\r
432 for (var i = 0; i < data.records.length; i++) {
\r
433 var r = data.records[i];
\r
434 var dup = this.store.getById(r.id);
\r
435 if (dup && (dd != this.dragZone)) {
\r
436 if(!this.allowDup && !this.allowTrash){
\r
437 Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1);
\r
440 var x=new Ext.data.Record();
\r
445 this.store.insert(insertAt++, r.copy());
\r
447 if (data.sourceView) {
\r
448 data.sourceView.isDirtyFlag = true;
\r
449 data.sourceView.store.remove(r);
\r
451 if(!this.allowTrash)this.store.insert(insertAt++, r);
\r
453 if(this.sortField){
\r
454 this.store.sort(this.sortField, this.sortDir);
\r
456 this.isDirtyFlag = true;
\r
458 this.dragZone.cachedTarget = null;
\r
463 onEndDrag: function(data, e) {
\r
464 var d = Ext.get(this.dragData.ddel);
\r
465 if (d && d.hasClass("multi-proxy")) {
\r
467 //delete this.dragData.ddel;
\r
472 removeDropIndicators : function(n){
\r
474 Ext.fly(n).removeClass([
\r
475 "x-view-drag-insert-above",
\r
476 "x-view-drag-insert-left",
\r
477 "x-view-drag-insert-right",
\r
478 "x-view-drag-insert-below"]);
\r
479 this.lastInsertClass = "_noclass";
\r
484 * Add a delete option to the DDView's context menu.
\r
485 * @param {String} imageUrl The URL of the "delete" icon image.
\r
487 setDeletable: function(imageUrl) {
\r
488 if (!this.singleSelect && !this.multiSelect) {
\r
489 this.singleSelect = true;
\r
491 var c = this.getContextMenu();
\r
492 this.contextMenu.on("itemclick", function(item) {
\r
495 this.remove(this.getSelectedIndexes());
\r
499 this.contextMenu.add({
\r
500 icon: imageUrl || AU.resolveUrl("/images/delete.gif"),
\r
502 text: AU.getMessage("deleteItem")
\r
507 * Return the context menu for this DDView.
\r
508 * @return {Ext.menu.Menu} The context menu
\r
510 getContextMenu: function() {
\r
511 if (!this.contextMenu) {
\r
512 // Create the View's context menu
\r
513 this.contextMenu = new Ext.menu.Menu({
\r
514 id: this.id + "-contextmenu"
\r
516 this.el.on("contextmenu", this.showContextMenu, this);
\r
518 return this.contextMenu;
\r
522 * Disables the view's context menu.
\r
524 disableContextMenu: function() {
\r
525 if (this.contextMenu) {
\r
526 this.el.un("contextmenu", this.showContextMenu, this);
\r
531 showContextMenu: function(e, item) {
\r
532 item = this.findItemFromChild(e.getTarget());
\r
535 this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true);
\r
536 this.contextMenu.showAt(e.getXY());
\r
541 * Remove {@link Ext.data.Record}s at the specified indices.
\r
542 * @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove.
\r
544 remove: function(selectedIndices) {
\r
545 selectedIndices = [].concat(selectedIndices);
\r
546 for (var i = 0; i < selectedIndices.length; i++) {
\r
547 var rec = this.store.getAt(selectedIndices[i]);
\r
548 this.store.remove(rec);
\r
553 * Double click fires the {@link #dblclick} event. Additionally, if this DDView is draggable, and there is only one other
\r
554 * related DropZone that is in another DDView, it drops the selected node on that DDView.
\r
556 onDblClick : function(e){
\r
557 var item = this.findItemFromChild(e.getTarget());
\r
559 if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) {
\r
562 if (this.dragGroup) {
\r
563 var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true);
\r
565 // Remove instances of this View's DropZone
\r
566 while (targets.indexOf(this.dropZone) !== -1) {
\r
567 targets.remove(this.dropZone);
\r
570 // If there's only one other DropZone, and it is owned by a DDView, then drop it in
\r
571 if ((targets.length == 1) && (targets[0].owningView)) {
\r
572 this.dragZone.cachedTarget = null;
\r
573 var el = Ext.get(targets[0].getEl());
\r
574 var box = el.getBox(true);
\r
575 targets[0].onNodeDrop(el.dom, {
\r
577 xy: [box.x, box.y + box.height - 1]
\r
578 }, null, this.getDragData(e));
\r
585 onItemClick : function(item, index, e){
\r
586 // The DragZone's mousedown->getDragData already handled selection
\r
587 if (this.ignoreNextClick) {
\r
588 delete this.ignoreNextClick;
\r
592 if(this.fireEvent("beforeclick", this, index, item, e) === false){
\r
595 if(this.multiSelect || this.singleSelect){
\r
596 if(this.multiSelect && e.shiftKey && this.lastSelection){
\r
597 this.select(this.getNodes(this.indexOf(this.lastSelection), index), false);
\r
598 } else if (this.isSelected(item) && e.ctrlKey) {
\r
599 this.deselect(item);
\r
601 this.deselect(item);
\r
602 this.select(item, this.multiSelect && e.ctrlKey);
\r
603 this.lastSelection = item;
\r
605 e.preventDefault();
\r