PDA

View Full Version : Stateful Portal



dpwhittaker
7 Aug 2008, 12:32 PM
It seems like a common request has been for a portal implementation that isn't such a pain to save state on. Something that keeps track of the position and status of the portlets, and can restore that state on the next page view. Well, here it is. Let me know what you think, and if there are any further features you might be interested in or bugs you find.

Features
==============================================================
Uses pre-configured classes with registered xtypes to define portlets
Recreates those portlets from their xtype, id, and initialConfig
This allows you to save custom state in initialConfig, or your own state management
All portlets are configurably closeable, resizeable, and collapsible
Settings option and handler provided to allow for easy custom user configuration
Example provided to help you figure out how it works
Example shows how to add items from buttons or drag-n-drop from a tree
Details
==============================================================

Much of the credit goes back to the Ext JS team, since I used the Portal sample in 2.0 as my starting point.

The concept of a stateful portal is simple. It stores state as a two dimensional array of the object configs needed to recreate that portlet, and then simply calls add on each of these panel configs.

What this means is that every portlet type needs to be implemented as an Extension class that extends some derivative of panel, and has the Ext.ux.PortletPlugin singleton class as one of its plugins. These extension classes then need to have an xtype defined for them. See the tutorial named "Writing a Big Application in Ext (http://extjs.com/learn/Tutorial:Writing_a_Big_Application_in_Ext)" for more details on this design approach. So far, it seems to be the model my applications have gravitated towards anyway, after a few months of using Ext. Either way, you don't have to use it for your whole application, just the portlets, if you don't want to.

One of the features of the PortletPlugin is that it re-IDs your component if you haven't given it one already. The format it uses is xtype-N, where N is an unique id number for that xtype and xtype is the xtype of the portlet. This gets it out of the ext-comp-N id system that might cause duplicate IDs when the component is recreated. The downfall is that you can't use this ID during the initComponent phase (since plugins are inited after initComponent exits), but anything you need to use the ID for can easily be processed in the beforerender event.

The reason why the IDs are reinstated upon creation is that you have the option of saving state in each portlet as well. For instance, if you derived one portlet from a GridPanel, and wanted to save the column width and order using the standard state mechanics, you can. Furthermore, since the plugin handles multiple instances of the same portlet by creating distinct IDs for them, you can easily have multiple copies of the same grid, each saving their own state. For example, consider a home page with a Weather portlet that saves the city information in the state. This would allow people to have multiple city's weather on their home page, and all the id management is done behind the scenes.

Another feature of the portlet is that the settings and close tools are now handled by the portlet (like the collapse tool already was). Now you can just set closeable to false to hide the close button, or set settings to true and define the settingHandler function to provide some method of setting values affect your portlet. The example shows how to open a Window to allow someone to input their name, and then save it either in the state of the portlet or of the portal (the portal saves xtype, id, and initialConfig to recreate the portlet, so portlets can save values in initialConfig and call saveState on the portal if it is easier than keeping track of their own state).

State information for a particular portlet is automatically deleted when the portlet is closed. This keeps a newly opened portlet from inheriting the state of the previous portlet of that type, and also saves space in your cookie, if that is the stateManager you set.

Portlets are also automatically resizeable (set resizeable = false to disable), and height information is saved back to the portal state.

The final caveat to watch out for is that you can't add portlets declaratively in the items config attribute of the columns. This is because they will be added to the state every time the page is loaded. So portlets must be dynamically loaded. However, you can set them declaratively in the state object by checking for to see if the stateId (or just id) of your portal exists in the state yet, and if not, adding it. The state information is structured as an array of column information, where each column is an array of portlet configs with xtypes. From the example:



var stateProvider = new Ext.state.CookieProvider({ expires: new Date(new Date().getTime()+(1000*60*60*24*365)) });
Ext.state.Manager.setProvider(stateProvider);

if (!stateProvider.get('home-portal'))
stateProvider.set('home-portal', [ [], [{id: 'required-main-portlet', xtype:'requiredportlet'}], [] ]);
This creates a portal with a single portlet of type 'requiredportlet' in the middle column of three. After this has been set, you can create the portal, and it will check its state and "restore" this first portlet back to the middle column.

There were a few bugs and behaviors I needed to change in Ext to make this work. See overrides.js for details. In short, I made three overrides:

I set stateful to false by default, and only stored state information when stateful is true (the current implementation always stores state information, and only restores when stateful is true).

I fixed a bug in Ext.state.Provider.decodeValue where it read empty arrays back out as [undefined].

I fixed a bug in Ext.Panel.onResize where when a collapsed panel was resized multiple times, the last of which only changed the width, then expanded, it would expand to its full height, even if previous calls to onResize had set the height. I also fixed an issue where when an animated panel that was first rendered as collapsed was expanded, it would expand to full height and then pop back to the correct height, by moving the resize from render to beforerender.

There is also a conditional bugfix on line 79 of Portlet.js. It fixes the "resizer proxy being rendered into the same container as the panel being resized" bug. This bug is fixed in version 2.2, but I left the fix in for people using previous versions and just checked for Ext.version < 2.2.

Sorry there is no live demo as of right now, I may set one up tonight. However, it is relatively simple to install. Just unzip the folder anywhere in your web directory. Then possibly change the path to ext-base.js and ext-all.js in the test.htm file.

Change log
==============================================================
08-11-2008:
Removed a console.log call... oops.

08-11-2008.1:
Added some functionality to the applyState function to make it remove the current state before applying. This allows people to apply a new state to the portal at any point by simply calling applyState with an array of arrays of portal settings, just as if they were setting it up the first time. I have added a button to show that to the demo, but here is the relevant code:



{xtype: 'button', text: 'Grid-Required-Dummy preload', cls: 'fullWidth',
handler: function(){
var portal = Ext.getCmp('home-portal');
portal.applyState([
[{xtype: 'samplegridportlet'}],
[{id: 'required-main-portlet', xtype:'requiredportlet'}],
[{xtype: 'dummyportlet'}]
]);
portal.doLayout();
}
}
You could easily save a configuration in state for later retrieval:



Ext.Msg.prompt('Save Portal State', 'Enter a label for your current configuration:', function (btn, label) {
if (btn == 'ok') {
var sp = Ext.state.Manager.getProvider();
sp.set(label, sp.get('home-portal'));
}
}
and later, when they choose to restore that state...


var sp = Ext.state.Manager.getProvider();
var portal = Ext.getCmp('home-portal');
portal.applyState(sp.get(label));
portal.doLayout();
09-26-2008:
Another bug fix. In IE6, when you use resizeable: true (default), the resizer tends to get stuck wherever it starts. You can still resize the panel, but the resize bar stays in the same spot. This is fixed by removing the dom element, and reappending it whenever a resize operation is completed. The zip file has been updated with these changes. If you have made your own changes, you can merge this version by finding the resizeElement: function() line at the bottom of portlet.js, and replacing the code in that function with the code in the same function in the zip file. That is, add these three lines before the saveState call and return box; at the bottom of Portlet.js:



//remove and reappend element to fix IE6 bug
var parent = this.south.el.parent();
this.south.el.remove();
parent.appendChild(this.south.el);

raphac
9 Aug 2008, 5:31 AM
Very very good.
But drag and drop not work in ie7.

Thanks....

kruxor
10 Aug 2008, 9:06 PM
Hi there,

Love your script! I have been looking for a save version of this for ages, I was just wondering is it easy to get another default window to always load on startup.

Would it also be hard to load or save different window sets via a button?

I have tried adding another window to the line:


stateProvider.set('home-portal', [ [], [{id: 'sampleTabPortlet', xtype:'sample-tab-portlet'}], [{id: 'required-main-portlet', xtype:'requiredportlet'}] ]);Using the box code:

var sampleTabPortlet = '<div id="tabs1"><div id="script" class="x-hide-display"><p><img src="images/chart.png"></p></div><div id="markup" class="x-hide-display"><p><img src="images/sampledata.png"></p></div><div id="tabthree" class="x-hide-display"><p><img src="images/chart.png"></p></div></div>';

DSR.SampleTabPortlet = Ext.extend(Ext.Panel, {
title: 'Sample Tab Portlet',
id: 'sample-tab-portlet',
closeable: true,
settings: false,
collapsible: false,
height: 232,
layout : 'form',
settingHandler: function(e, target, panel){
Ext.Msg.alert('Demo Setting', 'You clicked a dummy portlet setting for ' + this.id + '.');
},
html: sampleTabPortlet,
plugins: Ext.ux.PortletPlugin
});

Ext.reg('sampleTabPortlet', DSR.SampleTabPortlet);But all i get is a white screen.

dpwhittaker
11 Aug 2008, 8:49 AM
kruxor,

You Ext.regged 'sampleTabPortlet', but used xtype: 'sample-tab-portlet'. Make these consistent and it should work.

raphac,

Sorry, I left a console.log (firebug command) in there. You can remove it yourself from the bottom of test.js, or redownload portal.zip when I upload it in two minutes.

dpwhittaker
11 Aug 2008, 9:49 AM
Would it also be hard to load or save different window sets via a button?

I missed your other question, but yes, there is a way to load/save window sets via a button. I've added some more functionality that makes that really simple (just had applyState on the portal clear out the current state before applying the new state). Now all you have to do is call portal.applyState, passing it an array of arrays of portlet configs, just like the initial state settings.

Since the portal automatically tracks add/remove functions in the state variable, and applyState uses the standard add/remove functions, anything configuration you send to applyState will also replace the current state of the portal. In other words, you don't have to set the state first, then call applyState. Just call applyState, and it will set the state to whatever state you send it. Details added to the bottom of OP.

cboettch
14 Aug 2008, 8:25 AM
Thank you for your great work! But I've still a problem. It seems, that it is not possible to use the items parameter inside a Panel. Every time I try, i get the message 'this.items.add is not a function' from Firefox. For example, if I change the DummyPortlet in your code:


DSR.DummyPortlet = Ext.extend(Ext.Panel, {
title: 'Dummy Portlet',
closeable: true,
settings: true,
settingHandler: function(e, target, panel){
Ext.Msg.alert('Demo Setting', 'You clicked a dummy portlet setting for ' + this.id + '.');
},
html: shortBogusMarkup,
items: {html:"lorem ipsum"},
plugins: Ext.ux.PortletPlugin
});
Or am I doing something wrong?

Carsten

dpwhittaker
18 Aug 2008, 1:14 PM
@cboettch:

Sorry it took so long to get back with you, it seems my Thread Subscription isn't working...

I think you're having a little bit of trouble with the extension class concept. Here is a very basic layout of an extension class:



ClassConstructor = Ext.extend(Ext.Panel, {
prototype overrides (including custom functions, new functionality, etc.)...,
initComponent: function() {
Ext.apply(this, {
config overrides...
});
Ext.applyIf(this, {
config defaults...
});

ClassConstructor.superclass.initComponent.apply(this, arguments);

constructed object interactions (i.e. event handlers)...
}
});
So you see, you put the items array in the place of the prototype overrides, instead of in the place of the config overrides (which get applied to this before initComponent is called if a config is passed to the constructor). Container's initComponent actually saves this.items, deletes it, and replaces it with a MixedCollection of Components created from the saved this.items. However, if items is on the prototype instead of this, then delete this.items; can't delete it from the prototype, so you end up with your original array of items instead of the fancy MixedCollection of items we know and love. Consequently, this breaks a lot of other things.

So, your code should have looked more like this:



DSR.DummyPortlet = Ext.extend(Ext.Panel, {
title: 'Dummy Portlet',
closeable: true,
settings: true,
settingHandler: function(e, target, panel){
Ext.Msg.alert('Demo Setting', 'You clicked a dummy portlet setting for ' + this.id + '.');
},
html: shortBogusMarkup,
plugins: Ext.ux.PortletPlugin,
initComponent: function(){
this.items = {html:"lorem ipsum"};
DSR.DummyPortlet.superclass.initComponent.apply(this, arguments);
}
});
David

Scorpie
19 Aug 2008, 3:14 AM
Great work! Gonna try this asap!

cboettch
20 Aug 2008, 4:41 AM
@dpwhittaker

Thank you David, this helped a lot.

Carsten

KJedi
7 Sep 2008, 8:16 AM
There is an essential problem with this approach. If you have a component, that has complex initialConfig and that initial config has recurrent links (e.g. store->record->store), this approach fails because when you call set() method of the StateManager, it calls encodeValue method for each member of the object. So FF gives you error "too much recursion" there is no other way to cope with that other than to manage this by hands. So idea of the "stateful portal" is buried. That's quite sad because I like it, but unfortunately it doesn't work in real world :(

KJedi
8 Sep 2008, 7:05 AM
There is an essential problem with this approach. If you have a component, that has complex initialConfig and that initial config has recurrent links (e.g. store->record->store), this approach fails because when you call set() method of the StateManager, it calls encodeValue method for each member of the object. So FF gives you error "too much recursion" there is no other way to cope with that other than to manage this by hands. So idea of the "stateful portal" is buried. That's quite sad because I like it, but unfortunately it doesn't work in real world :(
However, it works if we do the following changes in the Portal.js:
Instead of (line 32):

state[i][j] = Ext.applyIf({xtype: p.getXType(), id: p.id}, p.initialConfig);
put:

state[i][j] = {xtype: p.getXType(), id: p.id, settingVals: p.getSettings(), height:p.getSize().height, title:p.title};
It remembers title of the panel and settings (all panels in my Portal are derived from one class, that has methods to work with settings. Each panel in my portal applies settings if they are specified)
This approach is not as beautiful as original one, but it works in any time. In fact, there is no need to remember all initialConfig as constructor is called when portlet is created from it's xtype.
Everything else is unchanged.

There are some more things you should remember about.
1) If you want to save state of the portal (and your current settings) from the portlet panel, you should write:

this.ownerCt.ownerCt.saveState();//update state of the portal
2) If you use Ext.Panel as base class for your portlets, you can't subscribe to 'close' event because Ext.Panel doesn't fire it. Alternative is either to use another base class or change Portlet.js. Online 53 instead of

handler: function(e, target, panel){
panel.ownerCt.remove(panel, true);
}
put

handler: function(e, target, panel){
panel.ownerCt.remove(panel, true);
if (typeof panel.onClose == 'function') panel.onClose(panel);
}

dpwhittaker
8 Sep 2008, 11:02 AM
There is an essential problem with this approach. If you have a component, that has complex initialConfig and that initial config has recurrent links (e.g. store->record->store), this approach fails because when you call set() method of the StateManager, it calls encodeValue method for each member of the object. So FF gives you error "too much recursion" there is no other way to cope with that other than to manage this by hands. So idea of the "stateful portal" is buried. That's quite sad because I like it, but unfortunately it doesn't work in real world :(

It is actually rather simple to handle this issue. Complex portlets should never simply be configured Panels, GridPanels, TreePanels, etc. They should be extended classes. There's nothing to say that you can't pass all the configuration parameters of a store in the config of your extended class, as long as they are simple, non-recursive objects or scalars.

Take a look at the testGridPortlet.js file in the sample. It creates a "pre-configured" GridPanel, with the store configured in the initComponent function rather than the initialConfig object. You could easily pass in the column configs, url (or even a data array), and any other variables needed to distinguish one instance of this particular portlet from another on the config, and let the initComponent do the actual data configuration.

This has many advantages for a stateful portal. First, it minimizes the data needed to be stored in a cookie or transmitted to a database. If only the truly unique information needs to be configured into the actual instance of the portlet, and the rest is taken care of by the subclass's initComponent, then only unique data is stored, and all that remains the same from one instance to the next is added at runtime.

Second, it follows a pattern already well established by the ExtJS community. The pre-configured class approach seemed a bit obtuse when I first started programming in Ext, but after trying other simpler approaches, nothing could match the organization and reusability of the pre-configured class. For instance, I have one StandardGrid class that every grid in my app configures, only passing the table name, title, and column information in the config. The initComponent then creates the columnModel, store, and pagingToolbar based on that column information. Later I came back and added an Export to Excel button in the StandardGrid class and in my server side code, and my whole site got an upgrade all at once. Any configuration of StandardGrid would work as a portlet.

Finally, any stateful object in Ext should be seen in terms of serialization. When you serialize a Word Document for instance, you don't write the entire in-memory data structure representing the document to disk. Likewise, when you save the state of a GridPanel, only its column widths, position, and sort Order are saved. In serialization, you write only the information needed to reconstruct the object. Ext gives us a powerful tool for minimizing the data needed to reconstruct objects in the form of extension and registration. These tools allow us to declare everything that defines a particular object, and serialize that definition as a simple {xtype: 'myClass'}. It further allows us to configure that extension, so it's not a rigid definition, but a starting point, by using {xtype: 'myClass', url: 'xyz.php'}, making the configuration available as this.url in initComponent. The stateful portal would have a lot of ground to cover to serialize the complex objects in Ext into something that will fit in a crowded cookie without using these already available and powerful tools.

I do use this portal in the real world, in a corporate production environment. I have a portlet that uses a typeahead combobox in the toolbar to filter a paging, grouping GridPanel. There are several objects with recursive references, but since they are all created in the extension class, and only the xtype, height, and previous search are configured, the portlet has no issue serializing this Component. I also have an explorer-style portlet with a dropdown treePanel and a paging, grouping grid, again with no issues, since all the complex configuration is done in the initComponent of the subclass of GridPanel, rather than the initialConfig. The stateful portal is far from buried if you use it correctly.

dpwhittaker
8 Sep 2008, 11:18 AM
...

There are some more things you should remember about.
1) If you want to save state of the portal (and your current settings) from the portlet panel, you should write:

this.ownerCt.ownerCt.saveState();//update state of the portal

You can also set stateful: true and define getState/applyState on your portlet, and just use this.saveState();
The portal automatically manages the id of the portlet to ensure its uniqueness, and clears the portlet's state information when it is closed.


2) If you use Ext.Panel as base class for your portlets, you can't subscribe to 'close' event because Ext.Panel doesn't fire it. Alternative is either to use another base class or change Portlet.js. Online 53 instead of

handler: function(e, target, panel){
panel.ownerCt.remove(panel, true);
}put

handler: function(e, target, panel){
panel.ownerCt.remove(panel, true);
if (typeof panel.onClose == 'function') panel.onClose(panel);
}

Or you can just subscribe to 'destroy'.

If you want beforeclose functionality with return false to cancel, then use this patch for that function.


handler: function(e, target, panel){
if(this.fireEvent("beforeclose", this) !== false){
panel.ownerCt.remove(panel, true);
this.fireEvent("close", this);
}
}

KJedi
10 Sep 2008, 8:15 AM
Sorry, I was really mistaken about that.
Yes, I'm using another approach for pre-configuring classes, but the one you show is better.
Mine is like following:

App.StaffSelection = function(config)
{
config = config || {};
this.createComponents();
config = Ext.applyIf(config, {
id:'staff-dialog',
title:txt.AddStuffTitle,
layout:'fit',
items:[this.WidgetList],
width:400,
height:300,
closeAction:'hide',
buttons:[{text:txt.Select, handler:this.onSelect, scope:this}]
});
App.StaffSelection.superclass.constructor.call(this, config);
}

Ext.extend(App.StaffSelection, Ext.Window, {
createComponents:function()
{
this.WidgetStore = App.Data.WidgetList;
this.WidgetStore.on('update', this.onRecordUpdate, this);
var tpl = new Ext.XTemplate(
'<tpl for=".">',
'<div class="thumb-wrap" id="{Name}">',
'<div class="thumb"><img src="{Image}" title="{Name}"></div>',
'<span>{Name}</span></div>',
'</tpl>',
'<div class="x-clear"></div>'
);
this.WidgetList = new Ext.DataView({
store:this.WidgetStore,
tpl:tpl,
autoHeight:true,
multiSelect:false,
singleSelect:true,
overClass:'x-view-over',
itemSelector:'div.thumb-wrap',
emptyText: 'No widgets to display'
});
}
,onSelect:function()
{
var recs = this.WidgetList.getSelectedRecords();
//create all widgets for the selected records
for (var i = 0; i < recs.length; i++)//normally there should be one item in the array
{
if (recs[i].get('Selected') === true)
{
Ext.Msg.alert(txt.Error, txt.WidgetAlreadySelected);
continue;
}
recs[i].set('Selected', true)
var wg = {
xtype:recs[i].get('WidgetID')
,title:recs[i].get('Name')
};
App.UI.Portal.addPanel(wg);
}
this.WidgetStore.commitChanges();
this.hide();
}
,onRecordUpdate:function(store, record, operation)
{
if (operation != Ext.data.Record.COMMIT) return;
if (!this.Conn)
{
this.Conn = new Ext.data.Connection({
url:App.url.UpdateWidgetState
});
}
this.Conn.request({
params:{
WidgetID:record.get('WidgetID'),
Selected:record.get('Selected')
}
});
}
});
I create all necessary components before component is created and then insert them into config. That's why I ran in such problems :(

Thank you for pointing me to the right direction :) and sorry about that comment.

KJedi
10 Sep 2008, 8:18 AM
You can also set stateful: true and define getState/applyState on your portlet, and just use this.saveState();
The portal automatically manages the id of the portlet to ensure its uniqueness, and clears the portlet's state information when it is closed.

Yes, but that will save state in another variable.
If we ask Portal to save it's state, it will save it to portal-x-type, if we call this.saveState() in panel, it will save it to panel-x-type variable.
My comment was about little tip on how to force Portal to update it's state.

swagner
10 Sep 2008, 8:30 AM
Good job, but it is a shame it doesn't work with IE7.

dpwhittaker
10 Sep 2008, 9:49 AM
Good job, but it is a shame it doesn't work with IE7.

Care to elaborate? It seems to work fine to me...

swagner
11 Sep 2008, 1:06 AM
Sorry, it was my fault. Looks like there was a path set wrong to a required file. Works fine now for me too.

accilies
21 Sep 2008, 3:18 PM
Take a look at the testGridPortlet.js file in the sample. It creates a "pre-configured" GridPanel, with the store configured in the initComponent function rather than the initialConfig object. You could easily pass in the column configs, url (or even a data array), and any other variables needed to distinguish one instance of this particular portlet from another on the config, and let the initComponent do the actual data configuration.


Thanks this helped a lot.

bwoody
25 Sep 2008, 12:55 PM
The stateful portal is great. Thank you. When running in IE6, there is a small bug when resizing a portlet. After making the portlet smaller by moving the resize bar, the Resize bar gets left in the position where it started before the resize. I can grab the bar (from its location below the portlet) and resize continues to work, but I am always left with this 'orphan' resize bar anytime I resize to a smaller size. Has anyone else seen this in IE6 and have a suggestion?

dpwhittaker
26 Sep 2008, 4:29 AM
The stateful portal is great. Thank you. When running in IE6, there is a small bug when resizing a portlet. After making the portlet smaller by moving the resize bar, the Resize bar gets left in the position where it started before the resize. I can grab the bar (from its location below the portlet) and resize continues to work, but I am always left with this 'orphan' resize bar anytime I resize to a smaller size. Has anyone else seen this in IE6 and have a suggestion?

The fix has been implemented and added to the code in the OP.

In short, you just have to remove and re-append the resize bar from the panel. IE6 positioning is frustrating at times.

accilies
3 Oct 2008, 7:13 AM
Hi,

I am trying to implement a REQUIRED portlet which will be not movable. I changed the Ext.apply in portlet.js to Ext.applyif because i had to overwrite the dragable param of the panel.



DSR.RequiredPortlet = Ext.extend(Ext.Panel, {
plugins: Ext.ux.PortletPlugin,
id: 'required-main-portlet',
title: 'Required Main Portlet',
collapsible: false,
closeable: false,
settings: false,
layout: 'fit',
draggable: false,
floating: false,
refresh: function(){
this.tpl.overwrite(this.body, this);
},
initComponent: function(){
this.tpl = new Ext.XTemplate(this.html), DSR.RequiredPortlet.superclass.initComponent.apply(this, arguments);

this.on('render', this.refresh, this);
},
stateful: true,
});

Ext.reg('requiredportlet', DSR.RequiredPortlet);
Doing this, I am able to not allow the portlet to be movable. However, if any other portlet is moved above this required portlet, its position is moved as to fit the new portlet in its position. How can i avoid this and make this portlet completely non movable from its position?
BEFORE
http://img71.imageshack.us/img71/7317/originalie2.jpg

MOVING
http://img361.imageshack.us/img361/4802/aftermoveet7.jpg

dpwhittaker
3 Oct 2008, 10:45 AM
Hi,

I am trying to implement a REQUIRED portlet which will be not movable. I changed the Ext.apply in portlet.js to Ext.applyif because i had to overwrite the dragable param of the panel.

...

Doing this, I am able to not allow the portlet to be movable. However, if any other portlet is moved above this required portlet, its position is moved as to fit the new portlet in its position. How can i avoid this and make this portlet completely non movable from its position?
BEFORE
http://img71.imageshack.us/img71/7317/originalie2.jpg

MOVING
http://img361.imageshack.us/img361/4802/aftermoveet7.jpg

The Portal fires an event named "validatedrop" with a single parameter called overEvent. overEvent is an object with the following members:



portal : the Ext.ux.Portal object
panel : the panel or portlet being dropped
columnIndex : the zero-based index of the column being dropped onto
column : the Ext.ux.PortalColumn being dropped onto
position : the position in the column that the portlet will become
data : the data object from the drag object
source : the drag object
rawEvent : the browser generated event
status : boolean whether or not to allow drop
From this, it should be relatively simple to create an event that will allow or disallow dropping based on almost any criteria. The following code is untested, but it should be something like this:



portal.on('validatedrop', function(overEvent) {
if (overEvent.columnIndex == 1 && overEvent.position == 0)
overEvent.status = false; //if in the middle column and first position, don't allow drop
return overEvent.status; //otherwise, defer to default dropAllowed property of dropZone
});


This prevents anything from being dropped at the top of the middle column. Change the numbers to fit your particular implementation.

accilies
3 Oct 2008, 11:51 PM
Thanks a load. This worked like a charm.


The Portal fires an event named "validatedrop" with a single parameter called overEvent. overEvent is an object with the following members:



portal : the Ext.ux.Portal object
panel : the panel or portlet being dropped
columnIndex : the zero-based index of the column being dropped onto
column : the Ext.ux.PortalColumn being dropped onto
position : the position in the column that the portlet will become
data : the data object from the drag object
source : the drag object
rawEvent : the browser generated event
status : boolean whether or not to allow drop
From this, it should be relatively simple to create an event that will allow or disallow dropping based on almost any criteria. The following code is untested, but it should be something like this:



portal.on('validatedrop', function(overEvent) {
if (overEvent.columnIndex == 1 && overEvent.position == 0)
overEvent.status = false; //if in the middle column and first position, don't allow drop
return overEvent.status; //otherwise, defer to default dropAllowed property of dropZone
});
This prevents anything from being dropped at the top of the middle column. Change the numbers to fit your particular implementation.

mcouillard
23 Oct 2008, 9:42 AM
Wonderful plugin, works great. One suggestion:

In my implementation there's a chance the # of portal columns may change over time. So the saved portal layout of 2008 may not match the portal layout of 2009. To prevent a JS error I've done the following:



...
applyState: function(state, config) {
...
for (var i = 0; i < state.length; i++) {
var col = this.items.items[i];
if (!col) continue; //skip when saved column doesn't match current column layout
...

thilo.knoetzele
19 Nov 2008, 5:36 AM
Hi David,

first of all thank you for this great implementation! Exactly what I was looking for after I have seen the first ExtJS Portal demo.

However I faced a couple of problems during implementation and - as I am still quite new to ExtJS - wondered if you could help me out here:

1. In your example, when I drag e.g. the "Grid Portlet" from the tree into the view, and then the "Dummy Portlet", for the second also the "Grid Portlet" appears. This means that after dragging one portlet to the portal, the next drags will always work with the first one. This happens both in FireFox and IE7. Any idea? Would be great to have this because it is a great feature!!

2. One requirement would be to allow adding a portlet only once. This means if it is already in the view, dragging and adding should not be allowed. Any idea how I can handle this? Register to a tree event?

3. The last problem is that I would like to show predefined DIVs as content in the portlets. With normal panels, this is possible with the contentEl attribute. But with the portlets, I am not able to get this working. Only the html attribute seams to work.

Thanks very much for our input!
Thilo

swagner
19 Nov 2008, 6:19 AM
2. You cound set a variable portletExists=true if the portlet gets rendered the first time and use this variable to cancel future renderings.


if (portletExists){
*rendering*
}


3. right, so you could render a full html-page inside the portlet, divs too (if they are inside html-tag+body)

thilo.knoetzele
19 Nov 2008, 6:49 AM
2. You cound set a variable portletExists=true if the portlet gets rendered the first time and use this variable to cancel future renderings.


if (portletExists){
*rendering*
}


3. right, so you could render a full html-page inside the portlet, divs too (if they are inside html-tag+body)

Hi Swagner,

thanks for the fast answer!

2. But where can I set this code? In the Davids portlet render code?

3. But why does the contentEl-attribute not work? Since I am creating a panel, it should? Here's my code:



DSR.DIV_1 = Ext.extend(Ext.Panel, { title: "Test", closeable: true, stateful: true, plugins: Ext.ux.PortletPlugin, initComponent: function() { this.items = { contentEl: "DIV_1" }; DSR.DIV_1.superclass.initComponent.apply(this, arguments); }});Ext.reg('xtype1 ', DSR.DIV_1);

swagner
19 Nov 2008, 7:26 AM
2. This is part of an own Portlet i use, it is loaded at first and when i click the checkbox a new portlet will be rendered. This code will render the portlet 'mailportlet' if this portlet does not already exist.



initComponent: function() {
this.items = [{
items: [{xtype: 'checkbox', boxLabel: 'Nachrichten', checked: val2, id: 'checkbox_mail',
handler: function(){
var component = Ext.getCmp('mailportlet');
if ( !component){
var portal = Ext.getCmp('home-portal');
portal.items.items[0].add({xtype:'mailportlet'});
portal.doLayout();
}
else if (component.hidden){
component.show();
}
else {
component.ownerCt.remove(component, true);
}
}
},


3. i do not know (i didn't work with the contentEl-attribute), but i did render a page with information about the mailstatus inside the 'mailportlet'

thilo.knoetzele
2 Dec 2008, 5:52 AM
Hi Swagner,

thanks for your input! This definately helped!
But has anyone some input to my first question? This is also not working in the demo:

1. In your example, when I drag e.g. the "Grid Portlet" from the tree into the view, and then the "Dummy Portlet", for the second also the "Grid Portlet" appears. This means that after dragging one portlet to the portal, the next drags will always work with the first one. This happens both in FireFox and IE7. Any idea? Would be great to have this because it is a great feature!!

Thanks and cheers
Thilo

kholy
20 Jan 2009, 7:01 AM
So using IE6 with 3 columns i am only able to drop items in the first 2 columns, for some reason it will not let you move an item to column 3, when you try to it moves below column 2.

Any ideas on a quick fix?

bwoody
20 Jan 2009, 7:39 AM
I've seen this type of thing happen in IE6 when all columns total 1 in width. So, for example, if your columns were set with widths: .33, .33, .34 you might see this in IE. I havent had any problem using .33, .33, 33 in IE6 though. or any set of widths such that the sum < 1.

kholy
20 Jan 2009, 7:44 AM
now also? if i wanted to use 2 columns, i was recieving a error, what is the best way to approach using just 2 columns?

bwoody
20 Jan 2009, 10:21 AM
Assuming that the .33 widths worked for you when you had 3 columns, usethis for two columns:

{ columnWidth: 0.49 }, { columnWidth: 0.50 }

kholy
25 Feb 2009, 11:16 AM
In IE if the portal page height expands more than is allowed, i get scrollbars not only for up and down, but also for left and right. Anyone have a solution? works fine in FF

kholy
26 Feb 2009, 6:17 AM
In IE if the portal page height expands more than is allowed, i get scrollbars not only for up and down, but also for left and right. Anyone have a solution? works fine in FF

this is for both IE7 and IE6, if anyone has any idea, let me know please :)

kholy
27 Feb 2009, 1:05 PM
this is for both IE7 and IE6, if anyone has any idea, let me know please :)

bump, i really need to solve this. - thanks!

MJFox
10 Mar 2009, 4:25 AM
1. In your example, when I drag e.g. the "Grid Portlet" from the tree into the view, and then the "Dummy Portlet", for the second also the "Grid Portlet" appears. This means that after dragging one portlet to the portal, the next drags will always work with the first one. This happens both in FireFox and IE7. Any idea? Would be great to have this because it is a great feature!!

I think I fixed that

in "portal.js" replace


if (!dd.panel) {
if (data.node && data.node.attributes.portlet)
dd.panel = data.node.attributes.portlet;
if (data.node && data.node.attributes.panel)
dd.panel = data.node.attributes.panel;
else if (data.panel)
dd.panel = data.panel;
else
return false;
}with


if (data.node && data.node.attributes.portlet)
dd.panel = data.node.attributes.portlet;
else if (data.node && data.node.attributes.panel)
dd.panel = data.node.attributes.panel;
else if (data.panel)
dd.panel = data.panel;
else
return false;Greetings

MJFox

panosru
18 Mar 2009, 7:22 PM
Hi! Greate extension! ;) Thanks a lot! It works fine, my only problem is that when i try to resize a portlet it start resize on mouse hold but on mouse release it doesn't stop the resizing process....

I tested this under FF3 and Safari 3

kholy
24 Mar 2009, 11:18 AM
In IE if the portal page height expands more than is allowed, i get scrollbars not only for up and down, but also for left and right. Anyone have a solution? works fine in FF


Any Ideas on this? still looking for a solution

jbull
2 Jul 2009, 3:03 AM
Can anyone tell me how I can remove a portal, update the state so that it's not resident in there, and then call a javascript function.

I've managed to use the portal example to generate portals in ASP.NET and now I need to be able to remove them. The javascript function is to postback to the page to remove the dataitem in my DB that ASP.NET uses when creating the portal.

asagala
30 Dec 2009, 8:26 PM
Anyone tried this with ExtJS 3? I am getting a lot of errors from both Firefox 3 and IE

tansu
6 Jan 2010, 5:34 AM
You can also use Kalitte Dynamic Dashboards for Asp.Net. Visit www.dynamicdashboards.net (http://www.dynamicdashboards.net) to see a live demo.

mcouillard
6 Jan 2010, 6:35 PM
Anyone tried this with ExtJS 3? I am getting a lot of errors from both Firefox 3 and IE

Yes, I've kept my local copy updated for 2.2 and now 3.0.3. This code contains all 3 JS files needed for a portal. Tested fairly well in FF3.5, IE7 and IE8.



Ext.ux.Portal = Ext.extend(Ext.Panel, {
layout: 'column',
autoScroll:true,
cls:'x-portal',
defaultType: 'portalcolumn',
initComponent : function(){
Ext.ux.Portal.superclass.initComponent.call(this);
this.addEvents({
validatedrop:true,
beforedragover:true,
dragover:true,
beforedrop:true,
drop:true
});
this.adjustForScrollbar();
this.on('resize', function() {this.lastCW = this.body.dom.clientWidth;}, this);
},

initEvents : function(){
Ext.ux.Portal.superclass.initEvents.call(this);
this.dd = new Ext.ux.Portal.DropZone(this, Ext.apply({ddGroup: 'portal'}, this.dropConfig));
},

beforeDestroy : function() { // ext 3.0.3
if(this.dd){
this.dd.unreg();
}
Ext.ux.Portal.superclass.beforeDestroy.call(this);
},

getState: function() {
var state = [];
for (var i = 0; i < this.items.length; i++) {
var col = this.items.items[i];
state[i] = [];
for (var j = 0; j < col.items.length; j++) {
var p = col.items.items[j];
state[i][j] = Ext.applyIf({xtype: p.getXType(), id: p.id}, p.initialConfig);
}
}
return state;
},

applyState: function(state, config) {
this.stateful = false;
//console.debug('applystate',state,config);
for (var i = 0; i < state.length; i++) {
var col = this.items.items[i];
while (col.items && col.items.length > 0) {
col.remove(col.items.items[0]);
}
for (var j = 0; j < state[i].length; j++) {
//console.debug('adding',state[i][j]);
col.add(state[i][j]);
}
}
this.stateful = true;
},

adjustForScrollbar: function() {
if (this.disabled)
this.on('enable', this.adjustForScrollbar, this);
else if (this.hidden)
this.on('show', this.adjustForScrollbar, this);
else if (!this.rendered)
this.on('render', this.adjustForScrollbar, this);
else
{
var cw = this.body.dom.clientWidth;
if(!this.lastCW){
this.lastCW = cw;
}else if(this.lastCW != cw){
this.lastCW = cw;
this.doLayout();
}
this.adjustForScrollbar.defer(100, this);
}
}
});
Ext.reg('portal', Ext.ux.Portal);


Ext.ux.Portal.DropZone = function(portal, cfg){
this.portal = portal;
Ext.dd.ScrollManager.register(portal.body);
Ext.ux.Portal.DropZone.superclass.constructor.call(this, portal.bwrap.dom, cfg);
portal.body.ddScrollConfig = this.ddScrollConfig;
};

Ext.extend(Ext.ux.Portal.DropZone, Ext.dd.DropTarget, {
ddScrollConfig : {
vthresh: 50,
hthresh: -1,
animate: true,
increment: 200
},

createEvent : function(dd, e, data, col, c, pos){
return {
portal: this.portal,
panel: data.panel,
columnIndex: col,
column: c,
position: pos,
data: data,
source: dd,
rawEvent: e,
status: this.dropAllowed
};
},

notifyOver : function(dd, e, data){
var xy = e.getXY(), portal = this.portal, px = dd.proxy;

// case column widths
if(!this.grid){
this.grid = this.getGrid();
}

// EXT 2.2: handle case scroll where scrollbars appear during drag
var cw = portal.body.dom.clientWidth;
if(!this.lastCW){
this.lastCW = cw;
}else if(this.lastCW != cw){
this.lastCW = cw;
portal.doLayout();
this.grid = this.getGrid();
}

// determine column
var col = 0, xs = this.grid.columnX, cmatch = false;
for(var len = xs.length; col < len; col++){
if(xy[0] < (xs[col].x + xs[col].w)){
cmatch = true;
break;
}
}
// no match, fix last index
if(!cmatch){
col--;
}

// find insert position ext 3.0.3
var p, match = false, pos = 0,
c = portal.items.itemAt(col),
items = c.items.items, overSelf = false;

for(var len = items.length; pos < len; pos++){
p = items[pos];
var h = p.el.getHeight();
if(h === 0){
overSelf = true;
}
else if((p.el.getY()+(h/2)) > xy[1]){
match = true;
break;
}
}

pos = (match && p ? pos : c.items.getCount()) + (overSelf ? -1 : 0);
var overEvent = this.createEvent(dd, e, data, col, c, pos);

if(portal.fireEvent('validatedrop', overEvent) !== false &&
portal.fireEvent('beforedragover', overEvent) !== false){

if (!px.getProxy)
{
if (p)
px.proxy = p.el.insertSibling({cls:'x-panel-dd-spacer'});
else
px.proxy = Ext.DomHelper.append(c.el.dom, {cls:'x-panel-dd-spacer'}, true);
px.getProxy = function() { return this.proxy; };
px.moveProxy = function(parentNode, before){
if(this.proxy){
parentNode.insertBefore(this.proxy.dom, before);
}
};
}

// make sure proxy width is fluid
px.getProxy().setWidth('auto');

if(p){
px.moveProxy(p.el.dom.parentNode, match ? p.el.dom : null);
}else{
px.moveProxy(c.el.dom, null);
}

this.lastPos = {c: c, col: col, p: match && p ? pos : false};
this.scrollPos = portal.body.getScroll();

portal.fireEvent('dragover', overEvent);

return overEvent.status;
}else{
return overEvent.status;
}

},

notifyOut : function(dd, e, data){
delete this.grid;
dd.proxy.getProxy().remove();
},

notifyDrop : function(dd, e, data){
delete this.grid;
if(!this.lastPos){
return false;
}
var c = this.lastPos.c, col = this.lastPos.col, pos = this.lastPos.p;
delete this.lastPos;

var dropEvent = this.createEvent(dd, e, data, col, c,
pos !== false ? pos : c.items.getCount());

if(this.portal.fireEvent('validatedrop', dropEvent) !== false &&
this.portal.fireEvent('beforedrop', dropEvent) !== false){

dd.proxy.getProxy().remove();

if (!dd.panel) {
/*
fix: http://extjs.com/forum/showthread.php?p=300823#post300823
*/
if (data.node && data.node.attributes.portlet)
dd.panel = data.node.attributes.portlet;
else if (data.node && data.node.attributes.panel)
dd.panel = data.node.attributes.panel;
else if (data.panel)
dd.panel = data.panel;
else
return false;
/*
end fix
*/
}

if (dd.panel.el)
dd.panel.el.dom.parentNode.removeChild(dd.panel.el.dom);

if(pos !== false){ // ext 3.0.3
if(c == dd.panel.ownerCt && (c.items.items.indexOf(dd.panel) <= pos)){
pos++;
}
c.insert(pos, dd.panel);
}else{
c.add(dd.panel);
}

this.portal.fireEvent('drop', dropEvent);

// scroll position is lost on drop, fix it
var st = this.scrollPos.top;
if(st){
var d = this.portal.body.dom;
setTimeout(function(){
d.scrollTop = st;
}, 10);
}

//The panel is hidden until after this function returns, but before afterDragDrop
//defining afterDragDrop here allows the column to resize the panel (and its contents) to its width
dd.afterDragDrop = function() {c.doLayout();};
return true;
}

return false;
},


// internal cache of body and column coords
getGrid : function(){
var box = this.portal.bwrap.getBox();
box.columnX = [];
this.portal.items.each(function(c){
box.columnX.push({x: c.el.getX(), w: c.el.getWidth()});
});
return box;
},

// EXT 2.2: unregister the dropzone from ScrollManager
unreg: function() {
//Ext.dd.ScrollManager.unregister(this.portal.body);
Ext.ux.Portal.DropZone.superclass.unreg.call(this);
}
});




/* MDC: portalcolumn */
Ext.ux.PortalColumn = Ext.extend(Ext.Container, {
layout: 'anchor',
//autoEl : 'div',//already defined by Ext.Component ext 3.0.3
defaultType: 'portlet',
cls:'x-portal-column',
initComponent : function() {
Ext.ux.PortalColumn.superclass.initComponent.apply(this, arguments);
this.on('remove', function(container, component) {
Ext.state.Manager.clear(component.stateId || component.id);
this.ownerCt.saveState.defer(100, this.ownerCt);
});
this.on('add', function() {
this.ownerCt.saveState.defer(200, this.ownerCt); // MDC increased to 200
});
this.on('change', function() {
//console.debug('portal change',arguments);
this.ownerCt.saveState.defer(300, this.ownerCt); // MDC increased to 300
});
}
});
Ext.reg('portalcolumn', Ext.ux.PortalColumn);

/* MDC: portlet */
Ext.ux.Portlet = Ext.extend(Ext.Panel, { // ext 3.0.3
anchor : '100%',
frame : true,
collapsible : true,
draggable : true,
cls : 'x-portlet'
});
Ext.reg('portlet', Ext.ux.Portlet);

Ext.ux.PortletPlugin = {
AUTO_IDs: {},
init: function(panel) {
//if you rely on auto-id's, then the ID in initComponent is invalid
//the reason for this is that the ID is reset here during the init of the plugin, which
//happens after initComponent is called.
//The issue is, as your page grows, or if people simply come to the portal later from
//another portion of the application, then the ext-comp id's may have already surpassed
//the ids for the stored portlets. This implementation gives the control an id of xtype-n
//allowing you to use multiple portlets of the same type, but allowing them to save state
//without risk of duplicating ids.
//If you need to use auto-ids in initComponent, just copy this block of code to your
//initComponent, changing this to Ext.ux.PortletPlugin
//You may have issues if you have multiple portals using the same portlets. If this is the
//case, you will need to design your own ID system to handle it. This is necessarily external
//to the Portlet, because Portlets are created before being added to any particular portal,
//and the best id system is probably something like PortalID-PortletXType-N
if (!this.AUTO_IDs[panel.xtype])
this.AUTO_IDs[panel.xtype] = 0;
if (panel.id.substring(0, 8) == 'ext-comp')
panel.id = panel.xtype + '-' + (++this.AUTO_IDs[panel.xtype]);
else if (panel.id.substring(0, panel.xtype.length) == panel.xtype)
this.AUTO_IDs[panel.xtype] = Math.max(this.AUTO_IDs[panel.xtype], parseInt(panel.id.substr(panel.xtype.length + 1)) + 1);

//required settings - these will override anything set up in config or initComponent of extension
//these attributes are required to make a portlet a portlet
Ext.apply(panel, {
anchor: '100%',
frame: true,
draggable: { ddGroup: 'portal' },
hideBorders: true,
cls: 'x-portlet'
});
//optional settings
Ext.applyIf(panel, {
collapsible: true,
settings: false,
settingHandler: Ext.emptyFn,
closeable: true,
resizeable: true,
tools: []
});

if (panel.settings) {
panel.tools.push({
id: 'gear',
handler: panel.settingHandler,
scope: panel,
qtip: 'Configure' //MDC added
});
}
if (panel.closeable) {
panel.tools.push({
id: 'close',
handler: function(e, target, panel) {
this.ownerCt.remove(panel, true);
}, panel);
}
});
}

//tell the Portal to doLayout after expanding or collapsing in case scrollbars appeared/disappeared
panel.on('expand', function() {
panel.initialConfig.collapsed = false;
panel.ownerCt.ownerCt.saveState();
panel.ownerCt.ownerCt.doLayout();
});

//the collapse event is deferred because panels that start collapsed fire the collapse event before rendering
panel.on.defer(50, panel, ['collapse', function() {
panel.initialConfig.collapsed = true;
panel.ownerCt.ownerCt.saveState();
panel.ownerCt.ownerCt.doLayout();
} ]);

if (panel.resizeable) {
panel.on('render', function() {
panel.resizer = new Ext.Resizable(panel.el, {
handles: 's',
minHeight: 100,
maxHeight: 800,
pinned: true,
resizeElement: function() {
var box = this.proxy.getBox();
panel.setSize(box);
panel.initialConfig.height = box.height;
//remove and reappend element to fix IE6 bug
if (Ext.isIE6) {
var parent = this.south.el.parent();
this.south.el.remove();
parent.appendChild(this.south.el);
}
panel.ownerCt.ownerCt.saveState();
return box;
}
});

//bug fix - Resizers place their proxy in the same container with the panel, messing with layouts
//this removes the proxy from the container and moves it to the end of the document. fixed in 2.2
if (Ext.version < 2.2) {
panel.resizer.proxy.remove();
panel.resizer.proxy.appendTo(Ext.getBody());
}
});
}
}
};

/* MDC: overrides */
//make components only save state when told to by stateful = true
Ext.override(Ext.Component, {
saveState : function(){
if(Ext.state.Manager && this.stateful !== false){
var state = this.getState();
if(this.fireEvent('beforestatesave', this, state) !== false){
Ext.state.Manager.set(this.stateId || this.id, state);
this.fireEvent('statesave', this, state);
}
}
},
stateful : false
});

//decodeValue erroneously decodes empty arrays and objects
//empty arrays return as [undefined]
//empty objects (untested) probably fail or return as {undefined: undefined}
//either way, a simple check for empty value portions alleviates this issue
Ext.override(Ext.state.Provider, {
decodeValue : function(cookie){
var re = /^(a|n|d|b|s|o)\:(.*)$/;
var matches = re.exec(unescape(cookie));
if(!matches || !matches[1]) return; // non state cookie
var type = matches[1];
var v = matches[2];
switch(type){
case "n":
return parseFloat(v);
case "d":
return new Date(Date.parse(v));
case "b":
return (v == "1");
case "a":
var all = [];
if (v) {
var values = v.split("^");
for(var i = 0, len = values.length; i < len; i++){
all.push(this.decodeValue(values[i]));
}
}
return all;
case "o":
var all = {};
if (v) {
var values = v.split("^");
for(var i = 0, len = values.length; i < len; i++){
var kv = values[i].split("=");
all[kv[0]] = this.decodeValue(kv[1]);
}
}
return all;
default:
return v;
}
}
});

//Panels added to the portal have their onResize function called twice. Once with width and height on creation,
//and again with only width when put into the ColumnLayout. If a panel is collapsed at creation, then the
//queuedBodySize object ends up with only the second call's data for width and height, effectively making panels
//that start collapsed autoSized when expanded the first time.
Ext.override(Ext.Panel, {
onResize : function(w, h){
if(w !== undefined || h !== undefined){
if(!this.collapsed){
if(typeof w == 'number'){
this.body.setWidth(
this.adjustBodyWidth(w - this.getFrameWidth()));
}else if(w == 'auto'){
this.body.setWidth(w);
}

if(typeof h == 'number'){
this.body.setHeight(
this.adjustBodyHeight(h - this.getFrameHeight()));
}else if(h == 'auto'){
this.body.setHeight(h);
}
//console.debug('panel resized',this);
}else{
//these two lines are the primary fix.
if (!this.queuedBodySize) this.queuedBodySize = {};
this.queuedBodySize = {width: w || this.queuedBodySize.width, height: h || this.queuedBodySize.height};
if(!this.queuedExpand && this.allowQueuedExpand !== false){
this.queuedExpand = true;
//switched this from expand to beforeexpand to keep the panel
//from expanding to full size, then popping back down to the correct size.
this.on('beforeexpand', function(){
delete this.queuedExpand;
this[this.collapseEl].show();
this.collapsed = false;
this.onResize(this.queuedBodySize.width, this.queuedBodySize.height);
this.doLayout();
}, this, {single:true});
}
}
this.fireEvent('bodyresize', this, w, h);
}
this.syncShadow();
}
});


Enjoy!

asagala
7 Jan 2010, 11:52 AM
thx


Yes, I've kept my local copy updated for 2.2 and now 3.0.3. This code contains all 3 JS files needed for a portal. Tested fairly well in FF3.5, IE7 and IE8.



Ext.ux.Portal = Ext.extend(Ext.Panel, {
layout: 'column',
autoScroll:true,
cls:'x-portal',
defaultType: 'portalcolumn',
initComponent : function(){
Ext.ux.Portal.superclass.initComponent.call(this);
this.addEvents({
validatedrop:true,
beforedragover:true,
dragover:true,
beforedrop:true,
drop:true
});
this.adjustForScrollbar();
this.on('resize', function() {this.lastCW = this.body.dom.clientWidth;}, this);
},

initEvents : function(){
Ext.ux.Portal.superclass.initEvents.call(this);
this.dd = new Ext.ux.Portal.DropZone(this, Ext.apply({ddGroup: 'portal'}, this.dropConfig));
},

beforeDestroy : function() { // ext 3.0.3
if(this.dd){
this.dd.unreg();
}
Ext.ux.Portal.superclass.beforeDestroy.call(this);
},

getState: function() {
var state = [];
for (var i = 0; i < this.items.length; i++) {
var col = this.items.items[i];
state[i] = [];
for (var j = 0; j < col.items.length; j++) {
var p = col.items.items[j];
state[i][j] = Ext.applyIf({xtype: p.getXType(), id: p.id}, p.initialConfig);
}
}
return state;
},

applyState: function(state, config) {
this.stateful = false;
//console.debug('applystate',state,config);
for (var i = 0; i < state.length; i++) {
var col = this.items.items[i];
while (col.items && col.items.length > 0) {
col.remove(col.items.items[0]);
}
for (var j = 0; j < state[i].length; j++) {
//console.debug('adding',state[i][j]);
col.add(state[i][j]);
}
}
this.stateful = true;
},

adjustForScrollbar: function() {
if (this.disabled)
this.on('enable', this.adjustForScrollbar, this);
else if (this.hidden)
this.on('show', this.adjustForScrollbar, this);
else if (!this.rendered)
this.on('render', this.adjustForScrollbar, this);
else
{
var cw = this.body.dom.clientWidth;
if(!this.lastCW){
this.lastCW = cw;
}else if(this.lastCW != cw){
this.lastCW = cw;
this.doLayout();
}
this.adjustForScrollbar.defer(100, this);
}
}
});
Ext.reg('portal', Ext.ux.Portal);


Ext.ux.Portal.DropZone = function(portal, cfg){
this.portal = portal;
Ext.dd.ScrollManager.register(portal.body);
Ext.ux.Portal.DropZone.superclass.constructor.call(this, portal.bwrap.dom, cfg);
portal.body.ddScrollConfig = this.ddScrollConfig;
};

Ext.extend(Ext.ux.Portal.DropZone, Ext.dd.DropTarget, {
ddScrollConfig : {
vthresh: 50,
hthresh: -1,
animate: true,
increment: 200
},

createEvent : function(dd, e, data, col, c, pos){
return {
portal: this.portal,
panel: data.panel,
columnIndex: col,
column: c,
position: pos,
data: data,
source: dd,
rawEvent: e,
status: this.dropAllowed
};
},

notifyOver : function(dd, e, data){
var xy = e.getXY(), portal = this.portal, px = dd.proxy;

// case column widths
if(!this.grid){
this.grid = this.getGrid();
}

// EXT 2.2: handle case scroll where scrollbars appear during drag
var cw = portal.body.dom.clientWidth;
if(!this.lastCW){
this.lastCW = cw;
}else if(this.lastCW != cw){
this.lastCW = cw;
portal.doLayout();
this.grid = this.getGrid();
}

// determine column
var col = 0, xs = this.grid.columnX, cmatch = false;
for(var len = xs.length; col < len; col++){
if(xy[0] < (xs[col].x + xs[col].w)){
cmatch = true;
break;
}
}
// no match, fix last index
if(!cmatch){
col--;
}

// find insert position ext 3.0.3
var p, match = false, pos = 0,
c = portal.items.itemAt(col),
items = c.items.items, overSelf = false;

for(var len = items.length; pos < len; pos++){
p = items[pos];
var h = p.el.getHeight();
if(h === 0){
overSelf = true;
}
else if((p.el.getY()+(h/2)) > xy[1]){
match = true;
break;
}
}

pos = (match && p ? pos : c.items.getCount()) + (overSelf ? -1 : 0);
var overEvent = this.createEvent(dd, e, data, col, c, pos);

if(portal.fireEvent('validatedrop', overEvent) !== false &&
portal.fireEvent('beforedragover', overEvent) !== false){

if (!px.getProxy)
{
if (p)
px.proxy = p.el.insertSibling({cls:'x-panel-dd-spacer'});
else
px.proxy = Ext.DomHelper.append(c.el.dom, {cls:'x-panel-dd-spacer'}, true);
px.getProxy = function() { return this.proxy; };
px.moveProxy = function(parentNode, before){
if(this.proxy){
parentNode.insertBefore(this.proxy.dom, before);
}
};
}

// make sure proxy width is fluid
px.getProxy().setWidth('auto');

if(p){
px.moveProxy(p.el.dom.parentNode, match ? p.el.dom : null);
}else{
px.moveProxy(c.el.dom, null);
}

this.lastPos = {c: c, col: col, p: match && p ? pos : false};
this.scrollPos = portal.body.getScroll();

portal.fireEvent('dragover', overEvent);

return overEvent.status;
}else{
return overEvent.status;
}

},

notifyOut : function(dd, e, data){
delete this.grid;
dd.proxy.getProxy().remove();
},

notifyDrop : function(dd, e, data){
delete this.grid;
if(!this.lastPos){
return false;
}
var c = this.lastPos.c, col = this.lastPos.col, pos = this.lastPos.p;
delete this.lastPos;

var dropEvent = this.createEvent(dd, e, data, col, c,
pos !== false ? pos : c.items.getCount());

if(this.portal.fireEvent('validatedrop', dropEvent) !== false &&
this.portal.fireEvent('beforedrop', dropEvent) !== false){

dd.proxy.getProxy().remove();

if (!dd.panel) {
/*
fix: http://extjs.com/forum/showthread.php?p=300823#post300823
*/
if (data.node && data.node.attributes.portlet)
dd.panel = data.node.attributes.portlet;
else if (data.node && data.node.attributes.panel)
dd.panel = data.node.attributes.panel;
else if (data.panel)
dd.panel = data.panel;
else
return false;
/*
end fix
*/
}

if (dd.panel.el)
dd.panel.el.dom.parentNode.removeChild(dd.panel.el.dom);

if(pos !== false){ // ext 3.0.3
if(c == dd.panel.ownerCt && (c.items.items.indexOf(dd.panel) <= pos)){
pos++;
}
c.insert(pos, dd.panel);
}else{
c.add(dd.panel);
}

this.portal.fireEvent('drop', dropEvent);

// scroll position is lost on drop, fix it
var st = this.scrollPos.top;
if(st){
var d = this.portal.body.dom;
setTimeout(function(){
d.scrollTop = st;
}, 10);
}

//The panel is hidden until after this function returns, but before afterDragDrop
//defining afterDragDrop here allows the column to resize the panel (and its contents) to its width
dd.afterDragDrop = function() {c.doLayout();};
return true;
}

return false;
},


// internal cache of body and column coords
getGrid : function(){
var box = this.portal.bwrap.getBox();
box.columnX = [];
this.portal.items.each(function(c){
box.columnX.push({x: c.el.getX(), w: c.el.getWidth()});
});
return box;
},

// EXT 2.2: unregister the dropzone from ScrollManager
unreg: function() {
//Ext.dd.ScrollManager.unregister(this.portal.body);
Ext.ux.Portal.DropZone.superclass.unreg.call(this);
}
});




/* MDC: portalcolumn */
Ext.ux.PortalColumn = Ext.extend(Ext.Container, {
layout: 'anchor',
//autoEl : 'div',//already defined by Ext.Component ext 3.0.3
defaultType: 'portlet',
cls:'x-portal-column',
initComponent : function() {
Ext.ux.PortalColumn.superclass.initComponent.apply(this, arguments);
this.on('remove', function(container, component) {
Ext.state.Manager.clear(component.stateId || component.id);
this.ownerCt.saveState.defer(100, this.ownerCt);
});
this.on('add', function() {
this.ownerCt.saveState.defer(200, this.ownerCt); // MDC increased to 200
});
this.on('change', function() {
//console.debug('portal change',arguments);
this.ownerCt.saveState.defer(300, this.ownerCt); // MDC increased to 300
});
}
});
Ext.reg('portalcolumn', Ext.ux.PortalColumn);

/* MDC: portlet */
Ext.ux.Portlet = Ext.extend(Ext.Panel, { // ext 3.0.3
anchor : '100%',
frame : true,
collapsible : true,
draggable : true,
cls : 'x-portlet'
});
Ext.reg('portlet', Ext.ux.Portlet);

Ext.ux.PortletPlugin = {
AUTO_IDs: {},
init: function(panel) {
//if you rely on auto-id's, then the ID in initComponent is invalid
//the reason for this is that the ID is reset here during the init of the plugin, which
//happens after initComponent is called.
//The issue is, as your page grows, or if people simply come to the portal later from
//another portion of the application, then the ext-comp id's may have already surpassed
//the ids for the stored portlets. This implementation gives the control an id of xtype-n
//allowing you to use multiple portlets of the same type, but allowing them to save state
//without risk of duplicating ids.
//If you need to use auto-ids in initComponent, just copy this block of code to your
//initComponent, changing this to Ext.ux.PortletPlugin
//You may have issues if you have multiple portals using the same portlets. If this is the
//case, you will need to design your own ID system to handle it. This is necessarily external
//to the Portlet, because Portlets are created before being added to any particular portal,
//and the best id system is probably something like PortalID-PortletXType-N
if (!this.AUTO_IDs[panel.xtype])
this.AUTO_IDs[panel.xtype] = 0;
if (panel.id.substring(0, 8) == 'ext-comp')
panel.id = panel.xtype + '-' + (++this.AUTO_IDs[panel.xtype]);
else if (panel.id.substring(0, panel.xtype.length) == panel.xtype)
this.AUTO_IDs[panel.xtype] = Math.max(this.AUTO_IDs[panel.xtype], parseInt(panel.id.substr(panel.xtype.length + 1)) + 1);

//required settings - these will override anything set up in config or initComponent of extension
//these attributes are required to make a portlet a portlet
Ext.apply(panel, {
anchor: '100%',
frame: true,
draggable: { ddGroup: 'portal' },
hideBorders: true,
cls: 'x-portlet'
});
//optional settings
Ext.applyIf(panel, {
collapsible: true,
settings: false,
settingHandler: Ext.emptyFn,
closeable: true,
resizeable: true,
tools: []
});

if (panel.settings) {
panel.tools.push({
id: 'gear',
handler: panel.settingHandler,
scope: panel,
qtip: 'Configure' //MDC added
});
}
if (panel.closeable) {
panel.tools.push({
id: 'close',
handler: function(e, target, panel) {
this.ownerCt.remove(panel, true);
}, panel);
}
});
}

//tell the Portal to doLayout after expanding or collapsing in case scrollbars appeared/disappeared
panel.on('expand', function() {
panel.initialConfig.collapsed = false;
panel.ownerCt.ownerCt.saveState();
panel.ownerCt.ownerCt.doLayout();
});

//the collapse event is deferred because panels that start collapsed fire the collapse event before rendering
panel.on.defer(50, panel, ['collapse', function() {
panel.initialConfig.collapsed = true;
panel.ownerCt.ownerCt.saveState();
panel.ownerCt.ownerCt.doLayout();
} ]);

if (panel.resizeable) {
panel.on('render', function() {
panel.resizer = new Ext.Resizable(panel.el, {
handles: 's',
minHeight: 100,
maxHeight: 800,
pinned: true,
resizeElement: function() {
var box = this.proxy.getBox();
panel.setSize(box);
panel.initialConfig.height = box.height;
//remove and reappend element to fix IE6 bug
if (Ext.isIE6) {
var parent = this.south.el.parent();
this.south.el.remove();
parent.appendChild(this.south.el);
}
panel.ownerCt.ownerCt.saveState();
return box;
}
});

//bug fix - Resizers place their proxy in the same container with the panel, messing with layouts
//this removes the proxy from the container and moves it to the end of the document. fixed in 2.2
if (Ext.version < 2.2) {
panel.resizer.proxy.remove();
panel.resizer.proxy.appendTo(Ext.getBody());
}
});
}
}
};

/* MDC: overrides */
//make components only save state when told to by stateful = true
Ext.override(Ext.Component, {
saveState : function(){
if(Ext.state.Manager && this.stateful !== false){
var state = this.getState();
if(this.fireEvent('beforestatesave', this, state) !== false){
Ext.state.Manager.set(this.stateId || this.id, state);
this.fireEvent('statesave', this, state);
}
}
},
stateful : false
});

//decodeValue erroneously decodes empty arrays and objects
//empty arrays return as [undefined]
//empty objects (untested) probably fail or return as {undefined: undefined}
//either way, a simple check for empty value portions alleviates this issue
Ext.override(Ext.state.Provider, {
decodeValue : function(cookie){
var re = /^(a|n|d|b|s|o)\:(.*)$/;
var matches = re.exec(unescape(cookie));
if(!matches || !matches[1]) return; // non state cookie
var type = matches[1];
var v = matches[2];
switch(type){
case "n":
return parseFloat(v);
case "d":
return new Date(Date.parse(v));
case "b":
return (v == "1");
case "a":
var all = [];
if (v) {
var values = v.split("^");
for(var i = 0, len = values.length; i < len; i++){
all.push(this.decodeValue(values[i]));
}
}
return all;
case "o":
var all = {};
if (v) {
var values = v.split("^");
for(var i = 0, len = values.length; i < len; i++){
var kv = values[i].split("=");
all[kv[0]] = this.decodeValue(kv[1]);
}
}
return all;
default:
return v;
}
}
});

//Panels added to the portal have their onResize function called twice. Once with width and height on creation,
//and again with only width when put into the ColumnLayout. If a panel is collapsed at creation, then the
//queuedBodySize object ends up with only the second call's data for width and height, effectively making panels
//that start collapsed autoSized when expanded the first time.
Ext.override(Ext.Panel, {
onResize : function(w, h){
if(w !== undefined || h !== undefined){
if(!this.collapsed){
if(typeof w == 'number'){
this.body.setWidth(
this.adjustBodyWidth(w - this.getFrameWidth()));
}else if(w == 'auto'){
this.body.setWidth(w);
}

if(typeof h == 'number'){
this.body.setHeight(
this.adjustBodyHeight(h - this.getFrameHeight()));
}else if(h == 'auto'){
this.body.setHeight(h);
}
//console.debug('panel resized',this);
}else{
//these two lines are the primary fix.
if (!this.queuedBodySize) this.queuedBodySize = {};
this.queuedBodySize = {width: w || this.queuedBodySize.width, height: h || this.queuedBodySize.height};
if(!this.queuedExpand && this.allowQueuedExpand !== false){
this.queuedExpand = true;
//switched this from expand to beforeexpand to keep the panel
//from expanding to full size, then popping back down to the correct size.
this.on('beforeexpand', function(){
delete this.queuedExpand;
this[this.collapseEl].show();
this.collapsed = false;
this.onResize(this.queuedBodySize.width, this.queuedBodySize.height);
this.doLayout();
}, this, {single:true});
}
}
this.fireEvent('bodyresize', this, w, h);
}
this.syncShadow();
}
});
Enjoy!

mnask79
15 Feb 2010, 11:06 AM
im trying to add chart panel




Ext.chart.Chart.CHART_URL = '../../resources/charts.swf';


var store2 = new Ext.data.JsonStore({
fields: ['season', 'total'],
data: [{
season: 'Summer',
total: 150
},{
season: 'Fall',
total: 245
},{
season: 'Winter',
total: 117
},{
season: 'Spring',
total: 184
}]
});

DSR.chart = Ext.extend(Ext.Panel, {
plugins: Ext.ux.PortletPlugin,
width: 400,
height: 400,
title: 'Pie Chart with Legend - Favorite Season',
stateful: true ,
items: {
store: store2,
xtype: 'piechart',
dataField: 'total',
categoryField: 'season',
//extra styles get applied to the chart defaults
extraStyle:
{
legend:
{
display: 'bottom',
padding: 5,
font:
{
family: 'Tahoma',
size: 13
}
}
}
}
});

Ext.reg('chart', DSR.chart);






stateProvider.set('home-portal', [ [{ xtype:'dummyportlet1'}], [{ xtype:'dummyportlet'}], [{ xtype:'chart'}] ]);



but it does not work , can u help me please ?

feendrache
24 Feb 2010, 4:06 AM
portal.on('validatedrop', function(overEvent) {
if (overEvent.columnIndex == 1 && overEvent.position == 0)
overEvent.status = false; //if in the middle column and first position, don't allow drop
return overEvent.status; //otherwise, defer to default dropAllowed property of dropZone
});
This prevents anything from being dropped at the top of the middle column. Change the numbers to fit your particular implementation.


Hey There this would solve my problem i'm currently having. I made the required-Portlet Part with succes... but not being the extjs pro i don't know where to put this part so it won't be moved by other containers.. can someone give me a quick help?

jshaw
6 Mar 2010, 9:05 PM
I am using Portal, PortalColumn, Portlet plugin in my application. This is working as expected in ext2.2. I am in the process of migrating the code stream to Ext3.1.1.
I took the latest code posted in this thread. Now when I try to add an (application) object to the portal I get:
----
too much recursion
encodeValue() ext-all-debug.js (line 41155)
setCookie() ext-all-debug.js (line 41305)
set() ext-all-debug.js (line 41277)
set() ext-all-debug.js (line 41216)
saveState() overrides.js (line 28)
apply() ext-base.js (line 7)
[Break on this error] flat += this.encodeValue(v[i]); ext-all-debug .js (line 41155)
-------------

Any help in resolving this will be much appreciated.

mcouillard
8 Mar 2010, 5:36 AM
too much recursion


I had the same problem until I added this function when getting and setting state:



function fctFixStateObject(oState) { // remove errant objects from the state object to minimize errors in Ext 3.1
//remove ownerCt from this object!! I have no idea why this is suddenly here with Ext 3.1, but it causes "Too Much Recursion" errors
if (oState.length > 0) {
for (var cols=0; cols<oState.length; cols++) {
for (var portlets=0; portlets<oState[cols].length; portlets++) {
oState[cols][portlets].ownerCt = [];
}
}
}
return oState;
}


This is called within the saveState override. Here's an example:



//make components only save state when told to by stateful = true
Ext.override(Ext.Component, {
saveState: function() {
if (Ext.state.Manager && this.stateful !== false) {
var state = this.getState();
state = fctFixStateObject(state);
//console.debug('saveState',arguments,',this state=',state,',stateid=',this.stateId || this.id);
Ext.state.Manager.set(this.stateId || this.id, state);
this.fireEvent('statesave', this, state);
}
},
stateful: false
});

jshaw
8 Mar 2010, 1:38 PM
In Ext3.1.1, I am noticing 2 issues:

1) In Portal.js, applyState function.
Here the state object provided by ext core, differs from Ext 2.x in the following manner.
Let's take the simplest case of a portal with just a dummy 'requiredportlet':
state = [ [], [{id: 'required-main-portlet', xtype:'requiredportlet'}], [] ]
If you break in applyState, the provided state argument is:
state = {0:[], 1:[{id: 'required-main-portlet', xtype:'requiredportlet'}], 2:[]}
i.e this is not an array as expected by the code.

I "hacked" the code as follows:
var stateNew = [];
for (x in state) {
if (Ext.isArray(state[x])) {stateNew.push(state[x])};
}
if you now use this stateNew instead of state, the requiredPortlet comes up.
Is this the right approach?

2) the solution provided to restrict recursion in Ext3.1 by setting ownerCt = [] seems to have the undesirable side effect of messing up the behavior of the component. Now if I drag and drop a component (say a chart portlet which is a panel that incorporates the Ext.ux.PortletPlugin, the chart no longer renders correctly - since the ownerCt has been set to [] in the state saving process..
I am not familiar with the core of this implementation - my humble opinion is that setting ownerCt to [] is not the right approach but instead, encodeValue() has to be modified to not recurse too deep unnecessarily..?

Your help in getting the portal to work correctly in 3.1.1 is greatly appreciated.

jshaw
8 Mar 2010, 3:21 PM
Please ignore my comment [2] above.
If I filter out ownerCt in getState(), it is working correctly.
I was reseting ownerCt in the wrong place.

My question (1) above still stands.

Thanks,

Jamie

jshaw
8 Mar 2010, 10:05 PM
I am doing more testing of our app's dashboard functionality (using Ext3.1.1) and I am running into problems while dragging and dropping components into the panel.

Some background info:
Dashboard is an extension of Ext.ux.Portal component and is configured as -


Dashboard.Portal = Ext.extend(Ext.ux.Portal, {
id:'dashboard-portal',
stateful: true,
initComponent: function() {
this.tbar = [{
text:'Save Configuration',
handler: ..
}];
this.items = [{
columnWidth:.33
},{
columnWidth:.33
},{
columnWidth:.33
}];
Dashboard.Portal.superclass.initComponent.apply(this, arguments);
},

onRender:function(){
Dashboard.Portal.superclass.onRender.apply(this, arguments);
},
....

The user should be able to drag/drop components and arrange them in any of the three columns.
[col 1 components] [col 2 components] [col3 components]
This is working correctly in Ext2.2 stream.

Now when I drag a component into column3 or try to drop a second component below the first in column 1 or 2, I get a cascade of errors in Firebug:
uncaught exception: [Exception... "Component returned failure code: 0x80004003 (NS_ERROR_INVALID_POINTER)" nsresult: "0x80004003 (NS_ERROR_INVALID_POINTER)" location: "JS frame :: .../Portal.js :: anonymous :: line 196" data: no]
-----
Here is the fragment of code:


187 if (!px.getProxy)
188 {
189 if (p)
190 px.proxy = p.el.insertSibling({cls:'x-panel-dd-spacer'});
191 else
192 px.proxy = Ext.DomHelper.append(c.el.dom, {cls:'x-panel-dd-spacer'}, true);
193 px.getProxy = function() { return this.proxy; };
194 px.moveProxy = function(parentNode, before){
195 if(this.proxy){
196 parentNode.insertBefore(this.proxy.dom, before); //HERE
197 }
198 };
199 }


Any suggestions/help in resolving this would be great.

Thanks,

Jamie

mcouillard
10 Mar 2010, 5:52 AM
1) In Portal.js, applyState function.
Here the state object provided by ext core, differs from Ext 2.x in the following manner.
Let's take the simplest case of a portal with just a dummy 'requiredportlet':
state = [ [], [{id: 'required-main-portlet', xtype:'requiredportlet'}], [] ]
If you break in applyState, the provided state argument is:
state = {0:[], 1:[{id: 'required-main-portlet', xtype:'requiredportlet'}], 2:[]}
i.e this is not an array as expected by the code.

I "hacked" the code as follows:
var stateNew = [];
for (x in state) {
if (Ext.isArray(state[x])) {stateNew.push(state[x])};
}
if you now use this stateNew instead of state, the requiredPortlet comes up.
Is this the right approach?


I, too, have hacked up applyState for 3.1.1 like this:


applyState: function(state, config) {
//console.debug('applyState',this,arguments);
var len = Object.size(state); // MC Ext 3.1; state is no longer an array but an object; OLD: len = state.length
this.stateful = false;
for (var i = 0; i < len; i++) {
var col = this.items.items[i];
if (!col) continue; // MC fix when saved columns don't match current column layout
while (col.items && col.items.length > 0) col.remove(col.items.items[0]);
for (var j = 0; j < state[i].length; j++) {
col.add(state[i][j]);
//console.debug('applied ',state[i],' to ',col);
}
}
this.stateful = true;
}


But this code also requires the Object.size function, found here:

Object.size = function(obj) { // get size of array-like object; http://stackoverflow.com/questions/5223/length-of-javascript-associative-array
var size = 0, key;
for (key in obj) {
if (obj.hasOwnProperty(key)) {
size++;
}
}
return size;
};

If your solution works too then I'd go with whatever method you're more comfortable with!

mm_202
21 Aug 2010, 9:24 PM
Has anyone made any progress on making this Ext 3.x (3.2.1) friendly?

mcouillard: Thank you for your code fix!
Now to just fix the Drag/Drop problem...

mcouillard
22 Aug 2010, 4:15 PM
Yes, back in May I made my copy 3.2.1 compatible. It's been working well ever since in our 2-column implementation with ~20 possible portlets (mostly grids and charts). Since the Sencha product and community has been so helpful to me, I'm more than happy to share what I've learned!

I also haven't encountered the drag/drop bug. Moving portlets seems to work fine, but I may not understand the true problem.

Here's the code, a compilation of portal.js, portlet.js and portalcolumn.js. This implementation doesn't save state on change, only on beforeunload. Just search for "MDC" to see my tweaks.



/* MDC portal.js */
Ext.ux.Portal = Ext.extend(Ext.Panel, {
layout: 'column',
autoScroll: true,
cls: 'x-portal',
defaultType: 'portalcolumn',

initComponent: function() {
Ext.ux.Portal.superclass.initComponent.call(this);
this.addEvents({
validatedrop: true,
beforedragover: true,
dragover: true,
beforedrop: true,
drop: true
});
//window.onbeforeunload = mdcDashboardUnload;
},

initEvents: function() {
Ext.ux.Portal.superclass.initEvents.call(this);
this.dd = new Ext.ux.Portal.DropZone(this, Ext.apply({ ddGroup: 'portal' }, this.dropConfig));
},

beforeDestroy : function() { /* 3.1 */
if(this.dd){
this.dd.unreg();
}
Ext.ux.Portal.superclass.beforeDestroy.call(this);
},

getState: function() {
//console.debug('getState',this,arguments);
var state = [];
for (var i = 0; i < this.items.length; i++) {
var col = this.items.items[i];
state[i] = [];
for (var j = 0; j < col.items.length; j++) {
var p = col.items.items[j];
state[i][j] = Ext.applyIf({ xtype: p.getXType(), id: p.id }, p.initialConfig);
}
}
return state;
},

applyState: function(state, config) {
//console.debug('applyState',this,arguments);
var len = Object.size(state); //MDC Ext 3.1; state is no longer an array but an object; OLD: len = state.length
this.stateful = false;
for (var i = 0; i < len; i++) {
//console.debug('apply? ',this.items.items[i]);
var col = this.items.items[i];
if (!col) continue; //MDC fix when saved columns don't match current column layout
while (col.items && col.items.length > 0) col.remove(col.items.items[0]);
for (var j = 0; j < state[i].length; j++) {
col.add(state[i][j]);
//console.debug('applied ',state[i],' to ',col);
}
}
this.stateful = true;
},

adjustForScrollbar: function() {
if (this.disabled)
this.on('enable', this.adjustForScrollbar, this);
else if (this.hidden)
this.on('show', this.adjustForScrollbar, this);
else if (!this.rendered)
this.on('render', this.adjustForScrollbar, this);
else {
var cw = this.body.dom.clientWidth;
if (!this.lastCW) {
this.lastCW = cw;
} else if (this.lastCW != cw) {
this.lastCW = cw;
this.doLayout();
}
this.adjustForScrollbar.defer(100, this);
}
}
});
Ext.reg('portal', Ext.ux.Portal);


Ext.ux.Portal.DropZone = Ext.extend(Ext.dd.DropTarget, {

constructor : function(portal, cfg){
this.portal = portal;
Ext.dd.ScrollManager.register(portal.body);
Ext.ux.Portal.DropZone.superclass.constructor.call(this, portal.bwrap.dom, cfg);
portal.body.ddScrollConfig = this.ddScrollConfig;
},
ddScrollConfig: {
vthresh: 50,
hthresh: -1,
animate: false, //MDC from true to improve IE8 performance on drag/drop of portlets w/chart
increment: 200
},

createEvent: function(dd, e, data, col, c, pos) {
return {
portal: this.portal,
panel: data.panel,
columnIndex: col,
column: c,
position: pos,
data: data,
source: dd,
rawEvent: e,
status: this.dropAllowed
};
},

notifyOver: function(dd, e, data) {
var xy = e.getXY(), portal = this.portal, px = dd.proxy;

// case column widths
if (!this.grid) {
this.grid = this.getGrid();
}

// handle case scroll where scrollbars appear during drag
var cw = portal.body.dom.clientWidth;
if (!this.lastCW) {
this.lastCW = cw;
} else if (this.lastCW != cw) {
this.lastCW = cw;
portal.doLayout();
this.grid = this.getGrid();
}

// determine column
var col = 0, xs = this.grid.columnX, cmatch = false;
for (var len = xs.length; col < len; col++) {
if (xy[0] < (xs[col].x + xs[col].w)) {
cmatch = true;
break;
}
}
// no match, fix last index
if (!cmatch) {
col--;
}

// find insert position
var p, match = false, pos = 0,
c = portal.items.itemAt(col),
items = c.items.items, overSelf = false;

for(var len = items.length; pos < len; pos++){
p = items[pos];
var h = p.el.getHeight();
if(h === 0){
overSelf = true;
}
else if((p.el.getY()+(h/2)) > xy[1]){
match = true;
break;
}
}

pos = (match && p ? pos: c.items.getCount()) + (overSelf ? -1: 0);
var overEvent = this.createEvent(dd, e, data, col, c, pos);

if (portal.fireEvent('validatedrop', overEvent) !== false &&
portal.fireEvent('beforedragover', overEvent) !== false) {

// make sure proxy width is fluid
px.getProxy().setWidth('auto');

if (p) {
px.moveProxy(p.el.dom.parentNode, match ? p.el.dom : null);
} else {
px.moveProxy(c.el.dom, null);
}

this.lastPos = {c: c, col: col, p: overSelf || (match && p) ? pos: false};
this.scrollPos = portal.body.getScroll();

portal.fireEvent('dragover', overEvent);

return overEvent.status;
} else {
return overEvent.status;
}

},

notifyOut: function(dd, e, data) {
delete this.grid;
//dd.proxy.getProxy().remove(); //3.1.1?
},

notifyDrop : function(dd, e, data){
delete this.grid;
if(!this.lastPos){
return;
}
var c = this.lastPos.c,
col = this.lastPos.col,
pos = this.lastPos.p,
panel = dd.panel,
dropEvent = this.createEvent(dd, e, data, col, c,
pos !== false ? pos : c.items.getCount());

if(this.portal.fireEvent('validatedrop', dropEvent) !== false &&
this.portal.fireEvent('beforedrop', dropEvent) !== false){

dd.proxy.getProxy().remove();
panel.el.dom.parentNode.removeChild(dd.panel.el.dom);

if(pos !== false){
c.insert(pos, panel);
}else{
c.add(panel);
}

c.doLayout();

this.portal.fireEvent('drop', dropEvent);

// scroll position is lost on drop, fix it
var st = this.scrollPos.top;
if(st){
var d = this.portal.body.dom;
setTimeout(function(){
d.scrollTop = st;
}, 10);
}

}
delete this.lastPos;
},

// internal cache of body and column coords
getGrid : function(){
var box = this.portal.bwrap.getBox();
box.columnX = [];
this.portal.items.each(function(c){
box.columnX.push({x: c.el.getX(), w: c.el.getWidth()});
});
return box;
},

// unregister the dropzone from ScrollManager
unreg: function() {
Ext.dd.ScrollManager.unregister(this.portal.body);
Ext.ux.Portal.DropZone.superclass.unreg.call(this);
}
});




/* MDC: portalcolumn.js */
Ext.ux.PortalColumn = Ext.extend(Ext.Container, {
layout: 'anchor',
//3.1 autoEl: 'div',
defaultType: 'portlet',
cls: 'x-portal-column',
initComponent: function() {
Ext.ux.PortalColumn.superclass.initComponent.apply(this, arguments);
this.on('beforestatesave',function() {
console.debug('cancel beforestatesave on portlets');
return false;
});
this.on('statesave',function() {
console.debug('cancel statesave on portlets');
return false;
});
this.on('remove', function(container, component) {
Ext.state.Manager.clear(component.stateId || component.id);
//this.ownerCt.saveState.defer(100, this.ownerCt); // MDC removed
});
this.on('add', function() {
//this.ownerCt.saveState.defer(100, this.ownerCt); // MDC removed
});
this.on('change', function() {
//if (!Ext.isIE) {
// console.debug('portal change',arguments);
// this.ownerCt.saveState.defer(100, this.ownerCt); // MDC removed
//}
});
}
});
Ext.reg('portalcolumn', Ext.ux.PortalColumn);



/* MDC: portlet.js */
Ext.ux.Portlet = Ext.extend(Ext.Panel, { // 3.2.1
anchor : '100%',
frame : true,
collapsible : true,
draggable : true,
cls : 'x-portlet'
});
Ext.reg('portlet', Ext.ux.Portlet);

Ext.ux.PortletPlugin = {
AUTO_IDs: {},
init: function(panel) {
//if you rely on auto-id's, then the ID in initComponent is invalid
//the reason for this is that the ID is reset here during the init of the plugin, which
//happens after initComponent is called.
//The issue is, as your page grows, or if people simply come to the portal later from
//another portion of the application, then the ext-comp id's may have already surpassed
//the ids for the stored portlets. This implementation gives the control an id of xtype-n
//allowing you to use multiple portlets of the same type, but allowing them to save state
//without risk of duplicating ids.
//If you need to use auto-ids in initComponent, just copy this block of code to your
//initComponent, changing this to Ext.ux.PortletPlugin
//You may have issues if you have multiple portals using the same portlets. If this is the
//case, you will need to design your own ID system to handle it. This is necessarily external
//to the Portlet, because Portlets are created before being added to any particular portal,
//and the best id system is probably something like PortalID-PortletXType-N
if (!this.AUTO_IDs[panel.xtype])
this.AUTO_IDs[panel.xtype] = 0;
if (panel.id.substring(0, 8) == 'ext-comp')
panel.id = panel.xtype + '-' + (++this.AUTO_IDs[panel.xtype]);
else if (panel.id.substring(0, panel.xtype.length) == panel.xtype)
this.AUTO_IDs[panel.xtype] = Math.max(this.AUTO_IDs[panel.xtype], parseInt(panel.id.substr(panel.xtype.length + 1)) + 1);
//console.debug('portletplugin init',arguments);
//required settings - these will override anything set up in config or initComponent of extension
//these attributes are required to make a portlet a portlet
Ext.apply(panel, {
anchor: '100%',
frame: true,
draggable: { ddGroup: 'portal' },
hideBorders: true,
cls: 'x-portlet'
});
//optional settings
Ext.applyIf(panel, {
collapsible: true,
settings: false,
settingHandler: Ext.emptyFn,
closeable: true,
resizeable: true,
tools: []
});

if (panel.settings) {
panel.tools.push({
id: 'gear',
handler: panel.settingHandler,
scope: panel,
qtip: 'Configure' //MDC added
});
}
if (panel.closeable) {
panel.tools.push({
id: 'close',
handler: function(e, target, panel) {
//MDC
Ext.Msg.confirm('Please confirm', 'Really delete this portlet?<br />(it can be re-added later)', function(btn) {
if (btn == 'yes') {
this.ownerCt.remove(panel, true);
} else {
return false; //to cancel the close action
}
}, panel);
},
qtip: 'Remove from dashboard' //MDC added
});
}
//console.debug('portletplugin partial');
//tell the Portal to doLayout after expanding or collapsing in case scrollbars appeared/disappeared
panel.on('expand', function() {
panel.initialConfig.collapsed = false;
//panel.ownerCt.ownerCt.saveState(); // MDC removed
panel.ownerCt.ownerCt.doLayout();
});

//the collapse event is deferred because panels that start collapsed fire the collapse event before rendering
panel.on.defer(50, panel, ['collapse', function() {
panel.initialConfig.collapsed = true;
//panel.ownerCt.ownerCt.saveState(); // MDC removed
panel.ownerCt.ownerCt.doLayout();
} ]);

if (panel.resizeable) {
panel.on('render', function() {
panel.resizer = new Ext.Resizable(panel.el, {
handles: 's',
minHeight: 100,
maxHeight: 1000,
pinned: true,
transparent: true,
resizeElement: function() {
//console.debug('panel resize',arguments);
var box = this.proxy.getBox();
panel.setSize(box);
panel.initialConfig.height = box.height; // required to save portlet height
panel.updateBox(box); // Ext 3.1
if (panel.layout) {
panel.doLayout();
}
//console.debug('panel resize',box,panel);
if (Ext.isIE6) {
//remove and reappend element to fix IE6 bug
var parent = this.south.el.parent();
this.south.el.remove();
parent.appendChild(this.south.el);
}
//panel.ownerCt.ownerCt.saveState(); // MDC removed
return box;
}
});
});
}
//console.debug('portletplugin done');
}
};

/* MDC: overrides */
//make components only save state when told to by stateful = true
Ext.override(Ext.Component, {
saveState: function() {
if (Ext.state.Manager && this.stateful !== false) {
var state = this.getState();
//state = mdcFixStateObject(state);
console.debug('saveState',arguments,',this state=',state,',stateid=',this.stateId || this.id);
Ext.state.Manager.set(this.stateId || this.id, state);
this.fireEvent('statesave', this, state);
}
},
stateful: false
});


//Panels added to the portal have their onResize function called twice. Once with width and height on creation,
//and again with only width when put into the ColumnLayout. If a panel is collapsed at creation, then the
//queuedBodySize object ends up with only the second call's data for width and height, effectively making panels
//that start collapsed autoSized when expanded the first time.
Ext.override(Ext.Panel, {
onResize: function(w, h) {
//console.debug('override panel resize',arguments);
if (w !== undefined || h !== undefined) {
if (!this.collapsed) {
if (typeof w == 'number') {
this.body.setWidth(
this.adjustBodyWidth(w - this.getFrameWidth()));
} else if (w == 'auto') {
this.body.setWidth(w);
}
if (typeof h == 'number') {
this.body.setHeight(
this.adjustBodyHeight(h - this.getFrameHeight()));
} else if (h == 'auto') {
this.body.setHeight(h);
}
//console.debug('panel resized',this);
} else {
//these two lines are the primary fix.
if (!this.queuedBodySize) this.queuedBodySize = {};
this.queuedBodySize = { width: w || this.queuedBodySize.width, height: h || this.queuedBodySize.height };
if (!this.queuedExpand && this.allowQueuedExpand !== false) {
this.queuedExpand = true;
//switched this from expand to beforeexpand to keep the panel
//from expanding to full size, then popping back down to the correct size.
this.on('beforeexpand', function() {
delete this.queuedExpand;
this[this.collapseEl].show();
this.collapsed = false;
this.onResize(this.queuedBodySize.width, this.queuedBodySize.height);
this.doLayout();
}, this, { single: true });
}
}
this.fireEvent('bodyresize', this, w, h);
}
this.syncShadow();
}
});

/**
* MDC: This function is used to delete indicators related to the portlet that is selected to be removed.
* Ideally if the delete fails, the portlet shouldn't be removed, due to SERIOUS conflict data issues.
* @param portletId
* @return boolean true=if no error messages were found, meaning that the delete was processed successfully; false if otherwise
*/
function deletePortletIndicator(obj, panel){
Ext.Ajax.request({
url:'employeeAnalysis.do'
//,timeout:Usermng.timeout //TODO

,params: {
dispatch: 'deletePortletIndicator'
}
,success: function(response,opts) {
processResponse(response, '');
//panel will be removed ONLY if indicators were deleted
if (response.responseText == '')
obj.ownerCt.remove(panel, true);
}
,failure: function(response,opts) {
//
}
});
}

razor
8 Sep 2010, 8:24 AM
@mcouillard:

Thank you very much for posting your code. I have copied the files and tried it on my own portal but no drag and drop functionality. Any chance we might be able to dig into this?

[edit] solution found

I found out why the Drag and Drop was not functioning. It seems that I had to add the following code to the Portlet.

draggable: { ddGroup: 'portal' },
stateful : true,
// draggable: true, // comment this out.

[questions]

1) My Firebug console mentions this: saveState [] ,this state= null ,stateid= ext-comp-1010
Why is the state undefined? What am I missing or doing wrong?

2)How can I make the PortletPlugin code get to work? initComponent references to which component/class/file? In other words, where should I copy it, or where should I call it?

Thank you again for your work-- Much appreciated!

Piruthu
25 Oct 2010, 10:59 PM
@mcouillard
Thank you very much for sharing.
I tried to implement this in a three column portel, but not able to save more than four portlets. In debug i found that 'getState' indeed is returning the actual number of portlets, but 'applyState' is getting maximum of four portlets. Am i missing something? Any help would be much appreciated.

Piruthu
26 Oct 2010, 9:43 PM
Sorry, i am new to Ext js and web development, Found out that each cookie is limited to 4 k in size.
Is there a way to split the cookie value for each portlet and save?

chuckburris07
17 Nov 2010, 2:33 PM
Razor,

Did you change the draggable attribute in the new Tree Node function or in the portlet extension class? I've made the changes and I receive a J.dom undefined error in Firefox.

Sam_1
6 Dec 2010, 3:13 AM
Hi,

I am new to Ext and trying to use drag n drop portal for one of my websites.
I have been trying to find a stateful portal and it seems that there already have been a lot of debate on it.
Can any one please provide me with one working example with the corresponding Ext to use.

Thanks,
Sam.

Sam_1
14 Dec 2010, 9:39 AM
I need a stateful combination of drag and dropable panel and drag/drop accross panel.
Will to pay reasonable amount.
Please let me know if any one has a solution and willing to sell.


Thanks,
SM