Update: I posted some news on this here.
Ever wondered how to create an interface like Google Personalized Home? In the first section of this article I'll demonstrate how to create a drag/drop portal in a few lines of JavaScript code, using the excellent Prototype and Scriptaculous JavaScript libraries. In the second section, I'll explain how to integrate this code into Drupal as a server backend for storing user settings. You may check the frontend here (tested with Firefox 1.5, IE6, and Opera 8.5), and download a reusable JavaScript Portal class and Drupal module for the backend at the bottom of this post.
Portal Frontend
Let's start with the XHTML structure of our portal, the portal will provide three columns for users to arrange their portlets, a list of available portlets to choose from, and the actual portlets. Each column has .portal-column as its class, and each portlet's class is .block (I'll refer to portlets as blocks from now on). The block list is going to be a special portal column identified by #portal-column-block-list as an id.
<div id="portal">
<div class="portal-column" id="portal-column-0">
<h2>Column 0</h2>
</div>
<div class="portal-column" id="portal-column-1">
<h2>Column 1</h2>
</div>
<div class="portal-column" id="portal-column-2">
<h2>Column 2</h2>
</div>
<div class="portal-column" id="portal-column-block-list" style="display: none;">
<h2 class="block-list-handle">Block List</h2>
<!-- Blocks go here -->
</div>
</div>The block list is not displayed until the user clicks on "Add content". Now let's take a look at each block's structure:
<div class="block">
<h3 class="handle">
<a class="block-toggle"><span>toggle</span></a>
<!-- Title -->
</h3>
<div class="content">
<!-- Content -->
</div>
</div>Toggle is a link to hide and show block's content.
Let's take a quick look at the relevant parts of the CSS code that styles our portal elements (I'll exclude margin, padding, background and such):
#portal .portal-column {
float: left;
width: 30%;
}
#portal #portal-column-block-list {
position: absolute;
width: 200px;
top: 180px;
left: 10px;
z-index: 10;
}
#portal .block .block-toggle {
background-image: url(block-slide.png);
float: right;
cursor: pointer;
}
#portal .block .block-toggle span {
display: none;
}
#portal .block-list-handle, #portal .handle {
cursor: move;
}We will use a three-column fluid layout. The block list is absolutely positioned (and will become draggable as we will see shortly). The block toggle button is styled as an image of a triangle, and mouse cursor on block handles is set to move, providing visual feedback that blocks are draggable.
Now for the fun part, making our portal interactive! I'll use the Sortable component of the Script.aculo.us library. This component represents a container with a list of draggable elements inside it, which makes it ideal for our portal interface. The JavaScript code starts with iterating over all elements with .portal-column class in the #portal div, making them sortables:
var sortables = document.getElementsByClassName(
'portal-column', 'portal'
);
sortables.each(function (sortable) {
Sortable.create(sortable, {
containment: sortables,
constraint: false,
tag: 'div',
only: 'block',
dropOnEmpty: true,
handle: 'handle',
hoverclass: 'block-hover',
})
});Sortable.create makes a sortable out of the element passed to it. It takes a list of options. tag and only specify the tag and class of draggable elements inside the sortable. Explanation of other options is available at Sortable.create documentation page.
Next step is activating the toggle button on blocks. To do this, we will iterate over elements with the .block class, and assign an event listener to each toggle:
var blocks = document.getElementsByClassName(
'block', 'portal'
);
blocks.each(
function (block) {
var content = Element.childrenWithClassName(
block, 'content', true
);
var toggle = Element.childrenWithClassName(
block, 'block-toggle', true
);
Event.observe(
toggle, 'click',
function (e) { Effect.toggle(content, 'Slide'); },
false
);/
}
);For each block, we retrieve the content and toggle elements inside it using Element.childrenWithClassName, then create an event listener that hides/shows content whenever toggle is clicked. We pass 'Slide' to Effect.toggle for a nice drawer effect.
The final step in creating our portal frontend is creating the "Add content" button (#portal-block-list-link) that shows the block list, and making the block list draggable:
Event.observe(
'portal-block-list-link', 'click',
displayBlockList,
false
);
new Draggable('portal-block-list', {
handle: 'block-list-handle'
}
);
function displayBlockList(e) {
Effect.toggle('portal-column-block-list');
Event.stop(e);
}And that's it! This is all what we need to make a portal with draggable portlets. You may see a the portal frontend with some sample portlets here. The page is extracted from Drupal and newsportal theme.
At the bottom of this post, there is a packaged version of this portal prototype. portal.js contains a reusable class for creating such portal interfaces. It has a constructor tht takes Prototype-like options object. The following options are available:
| Option | Default | Description |
|---|---|---|
| portal | portal | Id of the containing div for the portal. |
| column | portal-column | Class of portal columns. |
| block | block | Class of portal blocks (portlets). |
| content | content | Class of block's content div. |
| handle | handle | Class of block's handle. |
| hoverclass | block-hover | Hover class for blocks. |
| toggle | block-toggle | Class of block's toggle button. |
| blocklist | portal-column-block-list | Id of block list. |
| blocklistlink | portal-block-list-link | Id of block list toggle button. |
| blocklisthandle | block-list-handle | Class of block list handle. |
| saveurl | (none) | URL to submit column state to. |
In the next section, I'll outline how to save user settings to database by doing Ajax requests. Drupal will be the backend that handles data storage, integrating the code into any well-structured CMS that features the concept of blocks should be quite similar.
Portal Backend
Saving Settings
To monitor and submit user changes to server, we will use Sortable's onUpdate event handler. This function is called whenever a sortable's state is changed (by receiving or losing a block). This callback is defined in the options object passed to Sortable.create:
Sortable.create(sortable, {
/* Previously mentioned options here */
onUpdate: function (container) {
if (container.id == 'portal-column-block-list') {
return; // no need to save block list state.
}
var url = 'http://www.example.com/portal/save';
var postBody = container.id + ':';
var blocks = document.getElementsByClassName(
'block', container
);
postBody += blocks.pluck('id').join(',');
postBody = 'value=' + escape(postBody);
new Ajax.Request(url, {
method: 'post',
postBody: postBody
}
);
}
});
};The function creates a query of the form: value=container:block1,block2,block3 (...) and submits it to the server, this is done by iterating over the blocks of the changed container, it's worth noting that Sortable has a serialize method with similar functionality, however it requires special naming convention for element ids, I decided to write my own serialization function to make the code more flexible.
On the server side, Drupal will receive a POST request with the container state. Here is one way to handle it:
$query = split(':', $_POST['container']);
$container = $query[0];
$blocks = array();
if (!in_array($container, _portal_column_list())) {
return;
}
$available_blocks = _portal_block_list('list');
foreach (split(',', $query[1]) as $block) {
if (array_key_exists($block, $available_blocks)) {
$blocks[] = $block;
}
}
db_query("DELETE_FROM {portal_user_settings} WHERE user = '%s' AND container = '%s'", $uid, $container);
if ($blocks) {
db_query("INSERT_INTO {portal_user_settings} (user, container, blocks) VALUES ('%s', '%s', '%s')", $uid, $container, join(',', $blocks));
}The code extracts container and blocks ids into $container and $blocks, then verifies that submitted ids are actually available to portal users (to prevent malicious queries from submitting random strings into the database). _portal_column_list() and _portal_block_list('list') return arrays of available column and block ids respectively. If everything is correct, data is saved to the database.
Loading Settings
To load the settings on the client side, Drupal generates a JavaScript associative array that encompasses the settings and embeds it into the page:
$result = db_query("SELECT container, blocks FROM {portal_user_settings} WHERE user = '%s'", $uid);
$settings = array();
while ($row = db_fetch_object($result)) {
$blocks = array();
foreach (split(',', $row->blocks) as $block) {
$blocks[] = "\"$block\"";
}
$blocks = join(', ', $blocks);
$settings[] = "\"$row->container\": [$blocks]";
}
$settings = join(', ', $settings);
$output = '';
$output .= "\n<script type=\"text/javascript\"><!--\n";
$output .= "var settings = \{$settings};\n";
/* More JavaScript code added here */
$output .= "--></script>\n";
return $output;The resulting array will look something like:
var settings = { container1: [block1, block2], container2: [block3], ... };
settings variable is now available for our JavaScript code to rearrange blocks when the portal loads according to user preference:
for (var container in settings) {
settings[container].each(function (block) {
$(container).appendChild($(block));
});As you see, the code is pretty self-explanatory. It iterates over settings, and appends each block to its container.
This should cover the server side section. The code demonstrated here covers saving and loading user's portal settings. Attached below is the Drupal module from which code excerpts were taken. It's designed to nicely integrate with Drupal blocks. Any block can be used as a portlet, and developers need only implement hook_block to add portlets. Admins may specify what blocks are available as portlets, and both anonymous and registered users may maintain their own portals. The code is designed to gracefully degrade if JavaScript is disabled. If there is enough interest, I'll consider committing this module into Drupal's CVS and continue developing it.
Update: Drupal module was updated to fix an issue that prevented save from working when clean URLs were disabled.
| Attachment | Size |
|---|---|
| portal-frontend.tar.gz | 51.8 KB |
| drupal-portal-module-0.1.tar.gz | 164.11 KB |








DaveNotik (not verified) | Nice. :) | Mon, 2006/09/04 - 2:40am
Thrilled. Been wanting to do this for quite some time. Just today, revisited this need, and hours later I see your post.
I'd like to get this working for my Workspace service (http://www.wovenlabs.com/workspace), allowing every user to have a personalized Workspace home page. Using Views, I'd like to provide all kinds of blocks that the user can place -- latest issues, all issues assigned to me, latest posts within this particular project, favorite pages, and so on. Really an excellent use case.
I'm also building a service called MyCommunity.org -- an uber Yahoo Groups, if you will, and can imagine many uses there.
Can this be written to take advantage of jQuery, the JavaScript library that's now in Drupal core, or will these third-party libraries be needed?
Maybe I missed this: does your module handle keeping track of the placement of the blocks server-side -- so the user can log in elsewhere and see the page they created?
Thanks for your work. I'll keep following.
Ayman | Cool :) Glad you found this | Mon, 2006/09/04 - 2:04pm
Cool :) Glad you found this post interesting, regarding your questions: I'm more familiar with Prototype so I chose it to implement this, porting to jQuery is a tempting option now that it's part of Core, however it will still require a 3rd party plugin for drag/drop (the one Jeff linked to).
The module does actually save user settings, I edited the fist paragraph to clearly mention this.
Thanks for your feedback.
Blue Cobalt (not verified) | saving user settings | Mon, 2006/09/04 - 7:03pm
Is the Drupal module supposed to automatically save user settings?
It isn't doing so for me. I come back to the portal page, and there are no blocks saved.
Drupal 4.7.3, Firefox 1.5.0.6 & Safari 2.0.4, Mac OS X.
This is an incredible module. Thanks so much!
Ayman | I investigated a bit, and | Mon, 2006/09/04 - 7:34pm
I investigated a bit, and found an issue that prevented save from working when clean URLs were disabled, fixed it now, would you please try again?
Thanks for the feedback.
Blue Cobalt (not verified) | works great! | Mon, 2006/09/04 - 7:50pm
Thanks Ayman. It works great now!
Jeff Eaton (not verified) | jQuery | Mon, 2006/09/04 - 4:28am
Dave, if you're looking to implement something like this in jQuery you should probably check out http://interface.eyecon.ro/ -- it's a library of interface plugins for jQuery designed to duplicated prototype's functionality.
Mark (not verified) | brilliant | Mon, 2006/09/04 - 9:02am
This is excellent Ayman, I really hope you do make a drupal project/module for this. And it makes a lot of sense that the workspace module uses this as a base, as suggested by Dave. Thanks for a Great article sharing this.
Ayman | Thanks for your reply, and | Mon, 2006/09/04 - 2:06pm
Thanks for your reply, and indeed Workspace and this module can greatly benefit from each other, I'll research porting the JavaScript code to jQuery and creating a Drupal project for this.
Joe Moraca (not verified) | Very Cool | Mon, 2006/09/04 - 8:50pm
Great demo. I am going to try it out. Thanks for sharing your work on this project.
FiReaNG3L (not verified) | awesome! | Tue, 2006/09/05 - 1:42am
I can't wait to see your jQuery version... I was interested in building such a thing (but didn't have a clue of how-to). Very helpful!
Sami Khan (not verified) | cookies, blocks and wordpress | Tue, 2006/09/05 - 6:17pm
I think one should use cookies to store the settings for users not logged in, I believe that's what Google and Netvibes does. Using this on the blocks would be a prudent step as well. Most of these types of applications however are completely based in javascript, since the whole purpose is simply to aggregate content from many different sources on the user's browser all you need is the XMLHTTPRequest object. It looks pretty but in terms of functionality and purpose, it's rather limited. Also you have a problem in terms of being able to actively update the content in those blocks as it's not ajax, if Drupal had a more powerful AJAX framework to support the backend and then you could make AJAX calls to get that content, then such a portal solution might be cool and useful -- barring that it's rather limited and clunky to implement for an actual web application. Would be cool however if your users could reposition your blocks and get rid of the ones that they didn't want using this method as is the case with Wordpress Canvas. Thanks for sharing though, it might be useful to some people not all that familiar with the domain that just want some additional cool functionality on their site.
alf (not verified) | This seems great, I'll be | Wed, 2006/09/06 - 2:00pm
This seems great, I'll be looking forward to a JQuery version as well.
Dries (not verified) | Hot | Thu, 2006/09/07 - 8:29am
In one word: hot. Rock on, Ayman. Oh, and aim for core!
Ayman | Thanks, I did some work on | Thu, 2006/09/07 - 6:55pm
Thanks, I did some work on porting the code to jQuery today, will post to drupal.org once I have something working.
Jerry (not verified) | Interesting! | Sat, 2006/09/09 - 5:50am
I've been working on implementing a similiar CMS component just over the past week, and your demo just gave me a whole bunch of new ideas. Thanks!
Kudos for sharing your work, too :)
Mark (not verified) | panles module | Mon, 2006/09/18 - 1:08pm
I just discovered earl miles (merlin of choas) wonderful panels module, and it struck me that your UI would be just great for panels module. I may be wrong but it may be fairly easy to integrate your interface for use with his panels module. looking forward to news on your jquery version..
Ayman | I'm sorry for the delay, I | Mon, 2006/09/18 - 2:38pm
I'm sorry for the delay, I have something almost working in jQuery, I'm just trying to find time to polish and release it, hopefully I'll do so in the next few days.
I've just checked the panels module and indeed my work is a logical extension to it, thanks for pointing this out, we can discuss how to integrate them once I have the jQuery port ready.
Christoph Cemper (not verified) | thanks for sharing! | Sat, 2006/09/23 - 10:21am
Wow... this is an amazin post... and the sample rock too... thank you very much for sharing... you are now on my rss reader :-)
christoph
bugz_nz (not verified) | Great module! | Sun, 2006/09/24 - 10:16pm
Hey Ayman,
This is a great module, I look forward to customising it and using it soon. There seems to be a script error in the portal.module file on line 143.
it reads:
$output .= "var settings = \{$settings};\n";When I think it should read:
$output .= "var settings = {{$settings}};\n";Ayman | Hi, Thanks everyone for the | Mon, 2006/09/25 - 4:06pm
Hi,
Thanks everyone for the feedback, the last two weeks were very busy for me and I haven't had time to finish the jQuery port yet :(
bugz_nz, I double-checked, and looks like you are right, the syntax I used seems to work with versions prior to PHP 5.1(?), but for 5.1 it prints the backslash which is undesirable.
Ella (not verified) | Hey Ayman Thanks for the | Mon, 2006/09/25 - 6:32pm
Hey Ayman
Thanks for the great tutorial. I am having a bit of trouble setting the default blocks that I want to appear in the page when it first loads. I am using the following syntax but the line below:
var settings = { portal-column-0: [block1, block2], portal-column-1: [block4], portal-column-2: [block5] };keeps throwing a javascript error:
Error: Expected ':'
Am I missing a colon somewhere?
Many thanks,
Ella
Ella (not verified) | The error was due to the | Mon, 2006/09/25 - 8:14pm
The error was due to the hypens in the column names, I renamed my div id's and the following code now works:
var settings = {portalcolumn0: [block1, block2], portalcolumn1: [block4], portalcolumn2: [block5]};inanc (not verified) | one step further | Thu, 2006/09/28 - 3:30am
but, how to make a block not removed from block-list after dragged upon one of the three columns.
drag a to the middle column and drag a to rightmost column so we have got two a's, cloning and never disappears from the leftmost block list.
inanc (not verified) | two step further | Thu, 2006/09/28 - 3:31am
and... how to make a block removed from one of the columns (droppables) after clicked on a button on'em just like that toggle guy.
Hasan (not verified) | How to save moved block position within the same column | Thu, 2006/10/05 - 8:32am
Hi Ayman
This is the great module, thanks for that. I am having a bit problem. When I move a block within the same column then it does not post AJAX request. I am stuck on this, pls help.
Thanks,
Hasan
Anonymous (not verified) | Moving column in a same container | Sun, 2006/11/05 - 10:27am
Hi Hasan,
you can fix our problem by disabling the test of observer element in onEnd function : line 575 of dragdrop.js. For me it works fine now.
Thanks a lot for Ayman to this example.
Pat.
vik (not verified) | thanks | Tue, 2006/11/07 - 12:22am
Hi Ayman,
the tutorial is undoubtedly very good and i have been looking for something on these lines for quite a long time...many many thanks.
just 1 small query, instead of using DIV, can i use Table just by giving the appropriate elements the right id, class name.
i think that should be possible by changing the tag attribute in "sortables.create", i tried it but it did not work and throws an error 'addEventListener' is null or not an object. would be great if you can help.
Thanks vik
Jason (not verified) | onUpdate can't be invoked while draging in the same column | Wed, 2006/11/15 - 12:59pm
Hi Ayman, this is a really great demo
I have a question:
1) add two content to 'column0', the order is 'content0','content1'
2) if drag 'content0' to 'column1', the onUpdate is invoked, but while draging 'content0' in 'column0', the onUpdate will not be invoked
I don't know whether it is a bug, can you help me about this, ddrok@163.com
Thanks
Jason (not verified) | I think Pat's solution will | Thu, 2006/11/16 - 11:05am
I think Pat's solution will cause a performance issue, it will post all column's info to server
Jason (not verified) | Can you give me some help or | Sun, 2006/11/26 - 3:57pm
Can you give me some help or suggestions on this issue, with my great thanks
Post new comment