Проблема
ExtJS — прекрасная библиотека с огромным числом возможностей. На http://dev.sencha.com/deploy/dev/examples/ можно найти множество демонстрационных исходных кодов, доступных для использования в реальных проектах, однако, конечно, ответа на все вопросы это не даст.
Мне было необходимо сделать обоюдное перетаскивание между TreePanel и GridPanel. Найдя на форуме ExtJS и в интернете вообще лишь отрывочные сведения, я решил написать это самостоятельно. Как это у меня получилось — под катом.
Решение
Для начала стоит определиться — нам совершенно не нужно тащить за собой все файлы ExtJS. Поэтому HTML-файл, с которого все начинается, выглядит следующим образом:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="files/ext-all.css">
<script type="text/javascript" src="files/ext-base.js"></script>
<script type="text/javascript" src="files/ext-all.js"></script>
<script type="text/javascript" src="files/grid2treedrag.js"></script>
<title id="page-title">Drag&Drop между TreePanel и GridPanel в ExtJS</title>
</head>
<body>
<h3>Drag&Drop между TreePanel и GridPanel в ExtJS</h3>
</body>
</html>
* This source code was highlighted with Source Code Highlighter.
На этом с HTML покончено, и перейдем непосредственно к JavaScript. Для тестирования я предлагаю рассмотреть некую «корзину» пользователя, в левой части (GridPanel) — товары, в правой (TreePanel) — каталоги с ними.
Для начала создадим нашу сетку:
var grid1 = new Ext.grid.GridPanel({
store:new Ext.data.ArrayStore({
fields: ['name', 'unit', 'price'],
data: d
}),
columns:[
{
id: 'name_column',
header:"Наименование",
width:40,
sortable:true,
dataIndex:'name'
},{
id: 'unit_column',
header:"Ед. изм.",
width:20,
sortable:true,
dataIndex:'unit'
},
{
id: 'price_column',
header:"Цена",
width:30,
sortable:true,
dataIndex:'price'
}
],
sm : sm,
viewConfig:{
forceFit:true
},
id:'grid',
title:'Корзина',
region:'center',
layout:'fit',
enableDragDrop:true,
ddGroup:'grid2tree'
});
* This source code was highlighted with Source Code Highlighter.
d — это простой массив, использующийся в ArrayStore, вы, конечно, можете наполнять сетку в реальном приложении любым удобным вам способом. Самое главное в описании сетки — это enableDragDrop, установленное в true, и ddGroup:'grid2tree' — это название нашей группы перетаскивания. Для дерева она должна быть такой же.
Теперь создадим дерево:
var tree = new Ext.tree.TreePanel({
root:{
text:'Товары',
id:'root',
expanded:true,
children:[{
text:'Алкоголь',
children:[{
text: 'Виски',
leaf: true,
price: 7000,
unit: 'бут'
}, {
text: 'Коньяк',
leaf: true,
price: 5400,
unit: 'бут'
}, {
text: 'Слабоалкогольные напитки',
children:[{
text: 'Пиво',
leaf: true,
price: 7000,
unit: 'бут'
}]
}]
},{
text:'Продукты питания',
children:[{
text: 'Яблоки',
leaf: true,
price: 800,
unit: 'кг'
}]
},{
text:'Учебник по С++',
leaf:true,
price: 1200,
unit: 'шт'
}]
},
loader:new Ext.tree.TreeLoader({
preloadChildren:true
}) ,
enableDD:true,
ddGroup:'grid2tree',
id:'tree',
region:'east',
title:'Список товаров',
layout:'fit',
width:300,
split:true,
collapsible:true,
autoScroll:true,
listeners:{
beforenodedrop: function(e) {
if(Ext.isArray(e.data.selections)) {
if (e.target == this.getRootNode()) {
return false;
}
e.cancel = false;
e.dropNode = [];
var r;
for(var i = 0; i < e.data.selections.length; i++) {
r = e.data.selections[i];
e.dropNode.push(this.loader.createNode({
text:r.get('name'),
leaf:true,
price:r.get('price'),
unit: r.get('unit')
}));
r.store.remove(r);
}
return true;
}
}
}
});
* This source code was highlighted with Source Code Highlighter.
Строки
if (e.target == this.getRootNode()) {
return false;
}
можно закомментировать, если вы хотите разрешить перетаскивание объектов на корневой элемент из сетки. Как можно заметить, вначале проверяется, является ли массивом выделенных строк с данными (их можно выбирать несколько с помощью Shift и Ctrl) то, что нам дают на перетаскивание, затем в цикле пробегаемся по выделенным строкам и создаем ноды с нужными данными. Напоминаю, что «пользовательские» поля типа добавленых нами price и unit можно будет затем извлечь из attributes нода.
Прикрутим дополнительный флажок, цель которого — в демонстрации, и на который завязана дальнейшая логика, а также окошко для отображения нашей «корзины».
var cb = new Ext.FormPanel({
region: 'south',
frame: true,
height: 40,
labelWidth: 200,
labelPad: 0,
items: [
{
xtype: 'checkbox',
fieldLabel: 'Разрешить удалять каталоги',
listeners: {
check: function(cb, checked) {
remove_catalogs = checked;
}
}
}
]
});
// create and show the window
var win = new Ext.Window({
title:'Управление товарами',
id:'tree2divdrag',
border:false,
layout:'border',
width:700,
height:400,
items:[tree, grid1, cb]
});
win.show();
* This source code was highlighted with Source Code Highlighter.
Теперь создадим так называемый DropTarget для нашей сетки, то есть место, куда можно перетаскивать и кидать объекты.
var gridTargetEl = grid1.getView().scroller.dom;
Нам нужен элемент-контейнер для этого. Если посмотреть в исходники демок extJS или в ext-all-debug.js, то для GridView можно увидеть следующие строки:
ts.master = new Ext.Template(
'<div class="x-grid3" hidefocus="true">',
'<div class="x-grid3-viewport">',
'<div class="x-grid3-header"><div class="x-grid3-header-inner"><div class="x-grid3-header-offset" style="{ostyle}">{header}</div></div><div class="x-clear"></div></div>',
'<div class="x-grid3-scroller"><div class="x-grid3-body" style="{bstyle}">{body}</div><a href="#" class="x-grid3-focus" tabIndex="-1"></a></div>',
'</div>',
'<div class="x-grid3-resize-marker"> </div>',
'<div class="x-grid3-resize-proxy"> </div>',
'</div>'
);
* This source code was highlighted with Source Code Highlighter.
Это значит, что «тело» нашей сетки находится внутри скроллера, обрамляющего ее. Поэтому получим доступ к свойству scroller, описывающему этот враппер, и возьмем его DOM-содержимое.
Теперь, используя наш элемент, создадим саму DropTarget:
var GridDropTarget = new Ext.dd.DropTarget(gridTargetEl, {
ddGroup : 'grid2tree',
notifyDrop : function(ddSource, e, data) {
e.cancel = false;
var node = ddSource.dragData.node;
if ( ( (node.parentNode == null) || (!node.isLeaf() && !remove_catalogs) ) && !node.hasChildNodes() ) {
e.cancel = true;
return false;
}
var r = [];
if (!node.isLeaf()) {
node.cascade(function(n) {
var x = populate(n);
if (x != -1)
r.push(x);
});
}
else
r = populate(node);
grid1.store.add(r);
if ( (node.parentNode != null) && (remove_catalogs || !node.hasChildNodes()) ) {
node.remove();
}
else {
removeChildNodes(node);
}
return true;
}
});
* This source code was highlighted with Source Code Highlighter.
Вначале делаем проверки на то, корневой ли это узел, или можно ли удалять каталоги, причем все это должно не иметь потомков-нодов. В противном случае пробегаемся по списку нодов, если это не узел, или добавляем текущий узел, с помощью процедуры
var populate = function(node) {
if (!node.isLeaf()) return -1;
var r = new Ext.data.Record();
r.data.name = node.text;
r.data.price = node.attributes.price;
r.data.unit = node.attributes.unit;
return r;
}
* This source code was highlighted with Source Code Highlighter.
Она не добавляет «папки», а только узлы, с помощью вызова isLeaf(). Затем массив записей добавляется в хранилище сетки (extJS сам обновит ее), и начинают удаляться или не удаляться узлы с помощью процедуры
var removeChildNodes = function(node) {
node.expand();
for (var i = node.childNodes.length - 1; i >= 0; i--) {
var currentNode = node.childNodes[i];
if (currentNode.isLeaf() || remove_catalogs)
node.removeChild(currentNode);
else
removeChildNodes(currentNode);
}
}
* This source code was highlighted with Source Code Highlighter.
Без разворачивания узлов у меня не отработало их удаление — ноды в цикле проходят, но не удаляются, параметр destroy, установленный в true, также ничем не помог, равно как и различные вариации removeChild, expandChild и т.д. Поэтому буду рад любым замечаниям, предложениям и советам, которые услышу от много более опытного хабрасообщества.
Демо можно посмотреть здесь: http://www.linky.ru/~dima4ka/extjs/ (спасибо другу за фтп-доступ), там же можно скачать и исходники.
Полезные ссылки
http://dev.sencha.com/deploy/dev/docs/
http://www.sencha.com/forum/