Home » Extjs, Frameworking, Proâthuus
Drag and drop met panels in Extjs
23 September 2010 Geen reakties

enovision_0198 De documentatie over drag and drop in Extjs vond ik, na een aantal dagen mij intensief met het onderwerp bezig te hebben gehouden, op z'n zachtst gezegd nogal mager. Op internet kom je meestal dezelfde voorbeelden keer op keer weer tegen en veel daarvan behandelen grids en trees (bomen). Ik ga het hebben over panels, dus geen grids en trees, maar panels.

Wat is de bedoeling met dit artikel

Ik wil graag van de hoed en de rand weten hoe dingen werken en hou het dan ook graag eenvoudig. Extjs heeft haar drag en drop (DND) ondergebracht in ongeveer 11 klassen (!). Deze hebben betrekking op het eenvoudig  DND te kunnen doen met 1 object of hebben betrekking op het DND met meerdere objecten. Wij gaan enkelvoudig slepen, maar gebruiken de klassen voor het meervoudig slepen. Jesus Garcia heeft een heel mooi plaatje gemaakt, wat goed laat zien hoe de klassen zijn opgebouwd.

image

Zoals je ziet zijn de bovenste klassen voor "single" (enkelvoudig) slepen en de onderste 2 bubbels voor meervoudig slepen. Nu moet je beseffen dat veel van deze klassen weer extended klassen zijn van hun voorganger. Ik verwijs graag naar het prima artikel van Jesus Garcia (ook auteur van Extjs in Action, Manning publications). Ik ga het hier niet allemaal nog een keer uitleggen, wel laat ik zien wat ik heb gedaan met panels. Daarvoor kun je hier klikken voor de demo (opent in nieuw venster).

Demo

De demo heeft geen diepere functie, maar het laat wel mooi zien dat het allemaal niet zo moeilijk is.

enovision_0198

Zoals je ziet staan er allemaal bekende gezichten op het scherm (ontworpen voor 22" beeldschermen). Deze zijn allen sleepbaar (drag) en plaatsbaar (drop). Je kunt ze in de box linksboven droppen, je kunt ze ook verwisselen van plaats. In het drop venster linksboven, zie je het resultaat van je verrichtingen.

 

Zones

Ik heb een aantal dingen die ik in andere documentatie heb gevonden niet gebruikt (zoals ddGroup). Ook heb ik de  standaard "draggable" config optie op de panels uitgezet. Extjs verzorgt op basis van de config parameters een aantal dingen automatisch. Dat is ook een krachtig iets, maar hier zeker niet. Extjs kent namelijk ook een zogenaamde drag en drop voor Panels (ook weer via subklasses, waarvan enkele zelfs ongedocumenteerd). Essentieel voor DND met Extjs zijn de drag-zones en de drop-zones. Via deze zones communiceer je de regels voor het kunnen draggen en het mogen droppen. Ik heb 2 zones gedefinieerd, maar elke panel heeft in principe een "dragzone" en een "dropzone" (deze worden als 2 functies bij de render van de panels gedefinieerd).

De dropbox linksboven heeft alleen een "dropzone". De demo heeft tenslotte nog een button om de box leeg te maken.

Events

Ik kan slechts 1 ding zeggen wat nooit meer vergeten mag worden. HOU GRIP op je events. Elke event wordt door een Ext.util.Observer gemonitord en uitgevoerd. De volgorde heb je niet altijd onder controle. Zorg er daarom voor dat je niet ongelimiteerd events afvuurt (trigger), waarbij de situatie kan ontstaan dat de uitvoering ongecontroleerd  plaats vind, met ongewenste gevolgen.

Een ander belangrijk punt is, dat je er altijd van uit moet gaan dat jij de enige bent die weet wat elke methode of functie nodig heeft aan parameters. Ga hier zorgvuldig mee om. Ga niet graaien met firebug in de DOM om via trial en error de "juiste" waarden in je functies te krijgen. Als je een klasse (class) niet begrijpt, zorg dan eerst dat je hier documentatie over vind, zodat de context duidelijk wordt (niet dat ik nu alziend ben m.b.t. DND).

Extjs zelf

Extjs biedt veel mogelijkheden om events en listeners in te zetten. Maar pas hiervoor op! Want, alle listeners die worden toegevoegd in je eigen code werken als een override op de bestaande listeners (met gelijke naam), die in de broncode van Extjs al vaak de meest nuttige dingen uitvoeren. Als advies kan ik slechts geven, neem eens een kijkje (liefst wat langer en eigenlijk altijd bij de hand, als je ontwikkelt) in het bestand "ext-all-debug.js". Let op, niks aanpassen hier, want dat heeft geen enkele zin en leidt slechts tot een hoop narigheid.

   1: onInvalidDrop : function(target, e, id){

   2:     this.beforeInvalidDrop(target, e, id);

   3:     if(this.cachedTarget){

   4:         if(this.cachedTarget.isNotifyTarget){

   5:             this.cachedTarget.notifyOut(this, e, this.dragData);

   6:         }

   7:         this.cacheTarget = null;

   8:     }

   9:     this.proxy.repair(this.getRepairXY(e, this.dragData), this.afterRepair, this);

  10:  

  11:     if(this.afterInvalidDrop){

  12:         

  13:         this.afterInvalidDrop(e, id);

  14:     }

  15: },

  16:  

  17:  

  18: afterRepair : function(){

  19:     if(Ext.enableFx){

  20:         this.el.highlight(this.hlColor || "c3daf9");

  21:     }

  22:     this.dragging = false;

  23: },

  24:  

  25:  

  26: beforeInvalidDrop : function(target, e, id){

  27:     return true;

  28: },

Hierboven staat een klein stukje uit de klasse beschrijving van: Ext.extend(Ext.dd.DragSource, Ext.dd.DDProxy...

Zoals je ziet is dit een extensie op Ext.dd.DragSource, maar wat vooral belangrijk is, dat de events zo mooi zichtbaar worden. Hier zie je "onInvalidDrop" en "afterRepair". Wanneer jij nu deze events gaat gebruiken, d.w.z. herschrijven, dan wordt de code van hierboven overruled middels een override. Dus als je plaatje aan je muiscursor blijft kleven, dan weet je nu waarom.

   1: onBeforeDrag : function(data, e){

   2:        return true;

   3:    },

   4:  

   5:    

   6:    onStartDrag : Ext.emptyFn,

   7:  

   8:    

   9:    startDrag : function(x, y){

  10:        this.proxy.reset();

  11:        this.dragging = true;

  12:        this.proxy.update("");

  13:        this.onInitDrag(x, y);

  14:        this.proxy.show();

  15:    },

 

Nu heeft Extjs ook een aantal Ext.emptyFn events in haar code ondergebracht. Deze doen dus niks. Probeer te kijken of je misschien hier wat mee kunt, zonder een puinhoop van je toepassing te maken middels het overrulen van bestaande nuttige code. Je kunt natuurlijk ook zelf events schrijven bij je klasse en een eventmanager gebruiken. We doen het in dit artikel zonder. Schrijf eigen code nooit bij een event die veel en vaak wordt uitgevoerd, indien dit niet noodzakelijk is.image

In de Extjs documentatie staat keurig beschreven hoe vaak een event wordt afgevuurd.

 

Verander een variabele niet onnodig tig keer in een event welke bij elke muisbeweging wordt afgevuurd (denk dan aan het gebruiken van de "before" of "after" events).

Verklaring van de source

Het panel met filmsterren wordt geladen middels een array store. Dit kan dus ook wat anders zijn (bijvoorbeeld een JSON). Je kunt ook gewoon de panels met het handje fixed tikken. De panels zijn opgebouwd in de "center" region van een "border layout" en hebben zelf een "table" layout (geen discussie of dit beter met een data view, tree of grid gedaan had kunnen worden a.u.b.). In de source staat ccenter als variabele, ipv center. Dit lijkt vreemd en ongewenst, maar het woord "center" wordt in Microsoft Internet Explorer als een property gezien en zorgt er voor dat de toepassing niet goed wordt geladen in de browser!

   1: ccenter = Ext.getCmp('center');

   2:  

   3: ccenter.add ({

   4:     title           : 'Status and Drop Box',

   5:     id              : 'drop-panel',

   6:     iconCls         : 'help',

   7:     cls             : 'drop-zone',

   8:     colspan         : 2,

   9:     rowspan         : 2,

  10:     width           : 410,

  11:     height          : 410,

  12:     bbar            : {

  13:         height      : 40,

  14:         items       : [{

  15:             id      : 'btnClear',

  16:             width   :  100,

  17:             text    : 'Clear',

  18:             handler : function () {

  19:                 Ext.getCmp('drop-panel').update('')

  20:                 return

  21:             }

  22:         }]

  23:     },

  24:     autoScroll: true,

  25:     listeners: {

  26:         render: initDropZone

  27:     }

  28: });

Hierboven staat de definitie van de dropbox (linksboven). Je ziet hier een id: 'drop-panel' en een klasse (cls): 'drop-zone'. Onthoud goed dat je een klasse en een id niet klakkeloos door elkaar moet gebruiken. Een id (css id dus) is een unieke identificatie van een DOM element. Elke unieke id mag slechts 1x voorkomen in een web pagina. Een klasse (class) kan meerdere keren voorkomen en identificeert een groep van DOM elementen met gelijke bepaalde kenmerken. Een element kan ook gelijktijdig meerdere klassen hebben. In Extjs kun je ook een cls config zo opgeven "cls    :  'klasse1 klasse2 klasse3'.

De listener van dit panel verdient bijzondere aandacht, want hierin vind je de listener voor de render van het paneel. Daar wordt namelijk de initDropZone uitgevoerd. Dit moet altijd nadat een panel is gerendered. Eenvoudig weg, omdat er voordien nog geen panel is.

   1: store.each( function ( record ) {

   2:     ccenter.add ( {

   3:         title       : record.data.name,

   4:         id          : record.data.id,

   5:         html        : '<img src="moviestars/'+record.data.id+'.jpg" height="175" width="175" />',

   6:         iconCls     : getSexe ( record.data.sexe ),

   7:         cls         : 'drag-zone drop-zone',

   8:         listeners   : {

   9:             render: function ( e) {

  10:                 initDragZone( e );

  11:                 initDropZone( e )

  12:             }

  13:         }

  14:     })

  15: });

Hierboven zie je hoe de store (data) records worden doorlopen en de panels worden aangelegd met de filmsterren. Let ook hier op, want in de listener worden 2 methodes aangeroepen (initDragZone en initDropZone). De klasse (cls) is een dubbel klasse "drag-zone" en "drop-zone". Deze panels hebben namelijk een dubbelfunctie (draggen en droppen)

   1: function initDragZone (v) {

   2:     v.dragZone = new Ext.dd.DragZone(v.el.dom, {

   5:         getDragData : function(e) {

   7:             sourceEl = e.getTarget('.drag-zone');

   8:             if(sourceEl) {

   9:                 d = sourceEl.cloneNode(true);

  10:                 d.id = Ext.id();

  11:                 d.panel = v;               // we save the panel for later use

  12:                 return {

  13:                     sourceEl: sourceEl,

  14:                     repairXY: Ext.fly(sourceEl).getXY(),

  15:                     ddel : d

  16:                 }

  17:             }

  18:         },

  19:  

  20:         onStartDrag : function (e) {

  21:             this.dragger = Ext.getCmp(this.id).title;

  22:             Ext.getCmp('drop-panel').update ( updateStatus ( this.id, '', 'dragged' ) );

  23:         },

  24:  

  25:         onDrag: function(e) {

  26:             // !Important: manually fix the default position of Ext-generated proxy element

  27:             // Uncomment these line to see the Ext issue

  28:             var proxy = Ext.DomQuery.select('*', this.getDragEl());

  29:             proxy[2].style.position = ''

  30:         },

  31:  

  32:         afterInvalidDrop : function () {

  33:             Ext.getCmp('drop-panel').update ( updateStatus ( this.id, '', 'home' ) );

  34:             this.dragger = '';

  35:         },

  36:         getRepairXY: function() {

  37:             return this.dragData.repairXY;

  38:         },

  39:         onEndDrag : function ( e ) {

  40:             if (this.dragger !== '') {

  41:         // you can use this listener, for it is an emptyFN.

  42:         }

  43:         }

  44:     })

  45: }

Deze functie verzorgt de werking van de drag zones (dus de slepers, vergelijk het met vliegtuigen, bagage en luchthavens.

Deze functie is een verzameling van listeners, die het drag proces begeleiden:

getDragData, wordt aangeroepen om de informatie te verzamelen van het element wat wordt gesleept. Let op, Extjs sleept niet het echte element, maar een zogenaamde ghost, een kopie van het origineel, of iets wat middels een Ext.StatusProxy is meegegeven. Eerlijkheid gebiedt mij te zeggen dat ik daar nog niet veel wijzer van ben geworden, want mijn proxies gaven constant problemen (method CloneNode not found). Onze proxy laat het paneel zien met een ikoontje of je mag droppen of niet. De infornatie hiervoor ligt opgesloten in het "ddel" deel van de return bij getDragData (dit is dus de bagage). Interessant is nog dat ik d.panel heb opgenomen, want dan weet ik dat ik het gehele panel altijd beschikbaar heb voor later gebruik. Je mag zelf extra informatie opnemen.

 

onStartDrag is ook een lege listener die wordt aangesproken bij het starten van de sleep. Hier wordt een bericht gestuurd naar het drop-panel (linksboven), wat er wordt gesleept.

onDrag, wordt bij elke muisbeweging afgevuurd. Dit is wat ik hiervoor heb bedoeld, om een statement wat je eenmaal voor aanvang kan doen, dus niet in deze listener moet worden gestopt. De code die in deze listener staat is niet van mij zelf, maar als deze wordt weggelaten dan wordt de proxy niet geschoond, oftewel de sleep "ghost" wordt niet ge-reset.

afterInvalidDrop, wordt aangesproken als er wordt gedropped, maar de waar kan niet worden afgeleverd (het vliegtuig mag of kan niet landen).

getRepairXY, levert de waardes terug, om naar het origineel terug te animeren.

onEndDrag, spreekt bijna voor zichzelf. Je mag hier nog iets toevoegen om de drag af te sluiten. Mijn voorbeeld is leeg. Belangrijke tip blijft echter, laat geen lege listeners achter in je code.

   1: function initDropZone ( g ) {

   2:  

   3:     g.dropZone = new Ext.dd.DropZone(g.el.dom, {

   4:  

   5:         getTargetFromEvent: function(e) {

   6:             return e.getTarget('.drop-zone')

   7:         },

   8:         onNodeOver : function(target, dd, e, data) {

   9:             if (target.id != dd.id) {

  10:                 dd.el.addClass('drop-zone');

  11:                 return Ext.dd.DropZone.prototype.dropAllowed

  12:             } else {

  13:                 dd.el.removeClass('drop-zone');

  14:                 return Ext.dd.DropZone.prototype.dropNotAllowed

  15:             }

  16:         },

  17:         onNodeDrop : function(target, dd, e, data) {

  18:             // this is here, for sometimes the user is too fast

  19:             // you can not drop on yourself

  20:             if (target.id == dd.id) {

  21:                 dd.el.removeClass('drop-zone');

  22:                 return Ext.dd.DropZone.prototype.dropNotAllowed

  23:             }

  24:  

  25:             thishtml = Ext.getCmp('drop-panel').body.dom.innerHTML;

  26:             // host is "target"

  27:             // guest is data.ddel.panel.title

  28:             if (target.id == 'drop-panel') {

  29:                 Ext.getCmp('drop-panel').update ( updateStatus ( data.ddel.panel.id, '', 'dropped' ) );

  30:                 this.dragger = '';

  31:             } else {

  32:                 Ext.getCmp('drop-panel').update ( updateStatus ( data.ddel.panel.id, target.id, 'exchanged' ) );

  33:                 this.dragger = '';

  34:  

  35:                 // finally exchange the actors which have visited eachother

  36:                 host  = Ext.get(target.id);

  37:                 hostxy = host.getXY();

  38:                 guest = Ext.get(data.ddel.panel.id);

  39:                 guestxy = guest.getXY();

  40:                 guest.moveTo( hostxy[0], hostxy[1]);

  41:                 host.moveTo ( guestxy[0], guestxy[1]);

  42:             }

  43:             // data.ddel.panel.removeClass('drag-zone'); <- is not removed, for we can drag as much as we like

  44:             return (true)

  45:         }

  46:  

  47:     })

  48: }

De dropzone is de luchthaven en de code heeft de funktie van verkeerstoren.

getTargetFromEvent, vertelt of er al boven een landingsbaan wordt gevlogen. Deze is herkenbaar aan de klasse (cls) "drop-zone" (e.getTarget('drop-zone')).

onNodeOver, wordt getriggered wanneer het "vliegtuig" over de landingsbaan "vliegt". Dit wordt dus constant afgevuurd bij elke muisbeweging. Het "if" statement zorgt er voor dat er niet kan worden gedropped als er over "jezelf" wordt gevlogen (removeClass) en voegt in andere gevallen de klasse 'drop-zone' toe. De Ext.dd.Dropzone.prototype.dropAllowed en dropNotAllowed zorgen voor de juiste ikoontjes op de proxy (vliegtuig alias ghost element).

onNodeDrop, is waar het allemaal om draait. Nogmaals wordt er gekeken of er niet op jezelf wordt gedropped. De praktijk heeft mij geleerd dat deze event soms te snel was en dat er toch op zichzelf kon worden gedropped. Vervolgens wordt er wat informatie naar de linkerboven box verstuurd en speciale aandacht voor het verwisselen van de akteurs.

   1: // finally exchange the actors which have visited eachother

   2: host  = Ext.get(target.id);

   3: hostxy = host.getXY();

   4: guest = Ext.get(data.ddel.panel.id);

   5: guestxy = guest.getXY();

   6: guest.moveTo( hostxy[0], hostxy[1]);

   7: host.moveTo ( guestxy[0], guestxy[1]);

 

Fysiek wordt er in de DOM niks verwisseld. Dus geen panels weggegooid en nieuwe er tussen gefrommeld. Het enige wat er gebeurd, is dat de posities worden verwisseld. Dit zorgt er voor dat het optisch zeer snel gebeurd en dat de gebruiker er niks van merkt (behalve de gezichten die worden verwisseld). Misschien doe ik het wat omslachtig met host, hostxy en guest, guestxy, maar ik ben van mening dat ik het over een tijdje ook nog moet bebrijpen. Dus mijn lange ervaring zegt, liever een juist statement teveel dan een moeilijke structuur teveel.

Tenslotte

   1: function updateStatus ( host, guest, action ) {

   2:  

   3:     thishtml = Ext.getCmp('drop-panel').body.dom.innerHTML;

   4:     if (host != '') {

   5:         actor1 = Ext.getCmp(host).title

   6:         icoontje   = '<img src="moviestars/'+host+'.jpg" height="24" width="24" />';

   7:     } else {

   8:         return thishtml

   9:     }

  10:     if (guest != '') {

  11:         icoontje2   = '<img src="moviestars/'+guest+'.jpg" height="24" width="24" />';

  12:         actor2 = Ext.getCmp(guest).title

  13:     }

  14:  

  15:     return (action == 'dragged') ? '<p>'+icoontje+'&nbsp;'+actor1+' is dragged around</p>'+thishtml  :

  16:     (action == 'dropped')  ? '<p>'+icoontje+'&nbsp;'+actor1+' has been dropped</p>'+thishtml   :

  17:     (action == 'stopped')  ? '<p>'+icoontje+'&nbsp;'+actor1+' stopped dragging</p>'+thishtml :

  18:     (action == 'exchanged') ? '<p>'+icoontje+'&nbsp;'+actor1+' exchanged places with '+icoontje2+'&nbsp;'+actor2 + '</p>'+thishtml :

  19:     (action == 'home')     ? '<p>'+icoontje+'&nbsp;'+actor1+' is back home again</p>'+thishtml :

  20:     '<p>? something unexpected: '+action+'</p>'+thishtml;

  21: }

Hoewel het niets met DND te maken heeft, nog even hoe de messages in de box worden gemaakt. Een routine voor alle berichten.

 

Tips

Het kost veel moeite om alle documentatie door te lezen. Extjs heeft goede documentatie, maar niet altijd voldoende. Er zijn een aantal goede boeken, maar niet veel en altijd in het Engels. Verder zie ik nog te vaak dat de voorbeelden die worden gegeven, betrekking hebben op versie 2 van Extjs en ook vaak veel te moeilijk zijn.

  • Als het het framework niet goed kent, ga het bestuderen, voordat je verzuipt in zaken die bij voorbaat al moeizaam gaan.
  • Leer Javascript en begrijp de DOM. Niet van alleen van internet, maar vanuit een goed boek. Investeer een paar Euro's en eis er wat voor terug. Kijk ook eens bij Jesus Garcia's website. Hij doet veel moeite om mensen te helpen met deze materie.
  • Wees consistent in de naamgeving van je variabelen, methodes en events. CamelCase (GroteLettersDieKleineLettersAfwisselen) is erg mooi, maar ook erg foutgevoelig. Je mag best altijd kleine letters gebruiken (je kan echter niet ExtJS aanpassen, dus een beetje lijden blijft). En gebruik duidelijke namen. Niet zoiets als: ident32_212342, maar gewoon heldere namen die het gebruik duidelijk maken "hostxy", "icoontje". Bij twijfel geen woorden gebruiken die in conflict kunnen zijn met de Javascript syntax ( "complete", "override", "$ditisphp".
  • Gebruik firebug als ontwikkelhulp (hoewel ook Google Chrome een fantastische ontwikkelaars tool heeft) en laat het niet jou de baas zijn. Als je DOM elementen gebruikt, weet dan ook waarom. Niet dat je later javascript bugs hebt in jouw programma's, die door DOM-blunders zijn ontstaan. En test je programma ook in een andere browser dan Firefox. De DOM van Google Chrome is niet op alle punten compatible met Firefox, helemaal niet te spreken over Microsoft Internet Explorer. Dus blijf zo dicht mogelijk bij de Javascript "standards".
  • Gebruik zo min mogelijk trial en error code in je produktie programma's. Hou  je code zo schoon mogelijk. Maak prototypes (de demo is ook als dusdanig bedoeld) en gebruik voorbeelden die aanspreken. Als je uren bezig bent, spreekt "selma_hayek" nog altijd meer tot de verbeelding als "aa3-52gbalfa-iets". Maak je protos zorgvuldig (met plaatjes als plaatjes nodig zijn). Kost wat meer tijd, maar verkoopt veel beter.
  • Vraag hulp van anderen, maar laat anderen het niet voor jou doen (lees mijn artikel over web hufters op deze blog). Zorg dat je altijd baas blijft over de materie. Levert vanzelf bevredigende resultaten, mits je niet opgeeft.

Vraagteken

Nou wil ik graag nog wat hulp van de lezers. Er zit een bug in deze demo. Waar precies weet ik niet, maar het is zo dat ik een enkele keer heb geconstateerd dat de native browser drag van een afbeelding in Firefox, de baas was over de drag van de Javascript. De keren dat dit gebeurde was telkens met een niet gemaximaliseerd browser scherm.

 

Tenslotte zou ik het leuk vinden als er wat reacties komen, hoe deze DND te optimaliseren, of te vereenvoudigen. We hebben in mijn voorbeeld 2 drop gebieden gezien ( dropbox en filmsterren) en 1 drag gebied. Het is mij nog steeds niet helemaal duidelijk hoe het werkt met de ddGroup (vooral als een element naar meer kwaliteiten van zones moet kunnen worden gesleept.

Links

Leave your response!

Voeg je reaktie hieronder toe, of trackback van je eigen site. Je kunt ook inschrijven via RSS.

Reageer op het onderwerp, hou het netjes.

Je kunt deze labels gebruiken:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Deze weblog is Gravatar gevoelig. Voor een persoonlijke en wereldwijd herkende gravatar registeer op: Gravatar.

*

Get Adobe Flash playerPlugin by wpburn.com wordpress themes