Pull to refresh

Drag&Drop между TreePanel и GridPanel в ExtJS

Reading time11 min
Views6.4K

Проблема


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/
Tags:
Hubs:
+19
Comments9

Articles