Drag/Drop Portal Interface with Scriptaculous and Drupal

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 omit margin, padding, background and such):

#portal .portal-column {
  float: left;
  width: 30%;
}
#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:

<?php

$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:

<?php

$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=""><!--\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.

Comments

DaveNotik
DaveNotik's gravatar

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.


Posted at 1:40 a.m. on September 4, 2006

Jeff Eaton
Jeff Eaton's gravatar

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.


Posted at 3:28 a.m. on September 4, 2006

Mark
Mark's gravatar

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.


Posted at 8:02 a.m. on September 4, 2006

Ayman Hourieh
Ayman Hourieh's gravatar

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.


Posted at 1:04 p.m. on September 4, 2006

Ayman Hourieh
Ayman Hourieh's gravatar

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.


Posted at 1:06 p.m. on September 4, 2006

Blue Cobalt
Blue Cobalt's gravatar

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!


Posted at 6:03 p.m. on September 4, 2006

Ayman Hourieh
Ayman Hourieh's gravatar

I investigated a bit and found an issue that prevented saving from working when clean URLs were disabled. Fixed it now. Would you please try again?

Thanks for the feedback.


Posted at 6:34 p.m. on September 4, 2006

Blue Cobalt
Blue Cobalt's gravatar

Thanks Ayman. It works great now!


Posted at 6:50 p.m. on September 4, 2006

Joe Moraca
Joe Moraca's gravatar

Great demo. I am going to try it out. Thanks for sharing your work on this project.


Posted at 7:50 p.m. on September 4, 2006

FiReaNG3L
FiReaNG3L's gravatar

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!


Posted at 12:42 a.m. on September 5, 2006

Sami Khan
Sami Khan's gravatar

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.


Posted at 5:17 p.m. on September 5, 2006

alf
alf's gravatar

This seems great, I'll be looking forward to a JQuery version as well.


Posted at 1 p.m. on September 6, 2006

Dries
Dries's gravatar

In one word: hot. Rock on, Ayman. Oh, and aim for core!


Posted at 7:29 a.m. on September 7, 2006

Ayman Hourieh
Ayman Hourieh's gravatar

Thanks. I did some work on porting the code to jQuery today. Will post to drupal.org once I have something working.


Posted at 5:55 p.m. on September 7, 2006

Jerry
Jerry's gravatar

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 :)


Posted at 4:50 a.m. on September 9, 2006

Mark
Mark's gravatar

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..


Posted at 12:08 p.m. on September 18, 2006

Ayman Hourieh
Ayman Hourieh's gravatar

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.


Posted at 1:38 p.m. on September 18, 2006

Christoph Cemper
Christoph Cemper's gravatar

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


Posted at 9:21 a.m. on September 23, 2006

bugz_nz
bugz_nz's gravatar

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";

Posted at 9:16 p.m. on September 24, 2006

Ayman Hourieh
Ayman Hourieh's gravatar

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 it 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.


Posted at 3:06 p.m. on September 25, 2006

Ella
Ella's gravatar

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


Posted at 5:32 p.m. on September 25, 2006

Ella
Ella's gravatar

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]};


Posted at 7:14 p.m. on September 25, 2006

inanc
inanc's gravatar

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.


Posted at 2:30 a.m. on September 28, 2006

inanc
inanc's gravatar

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.


Posted at 2:31 a.m. on September 28, 2006

Hasan
Hasan's gravatar

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


Posted at 7:32 a.m. on October 5, 2006

Anonymous
Anonymous's gravatar

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.


Posted at 9:27 a.m. on November 5, 2006

vik
vik's gravatar

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


Posted at 11:22 p.m. on November 6, 2006

Jason
Jason's gravatar

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


Posted at 11:59 a.m. on November 15, 2006

Anonymous
Anonymous's gravatar

Ayman,

I uploaded the portal module in my drupal modules directory and got it enabled. I could see the three columns in the portal, but when I chose "add content" it said that the portal - block list is not implemented. I just couldn't seem to figure out how to get that implemented.

Thanks Nic


Posted at 12:36 a.m. on November 16, 2006

Jason
Jason's gravatar

I think Pat's solution will cause a performance issue, it will post all column's info to server


Posted at 10:05 a.m. on November 16, 2006

Ayman Hourieh
Ayman Hourieh's gravatar

1) Do you have JavaScript enabled in your browser?

2) What browser are you using?

3) Does the demo hosted here work?


Posted at 10:33 p.m. on November 17, 2006

Jason
Jason's gravatar

Can you give me some help or suggestions on this issue, with my great thanks


Posted at 2:57 p.m. on November 26, 2006

Anonymous
Anonymous's gravatar

Hi!

This is great! :) :)

Could you tell me though, how you are parsing RSS Feeds? Are you doing it yourself or is there some ready-made code online... if yes, i'd be grateful if you could share it..!(especially if it is in Javascript...) Many Thanks!

S


Posted at 6:47 a.m. on December 5, 2006

Timmy
Timmy's gravatar

This: http://www.geocities.com/timmy33479//demo.html is what I am trying to do. What is the easiest way to save the positions of the yellow squares on the server and without a database? I am just starting to mess with Scriptaculous, but I have to have a demo of this ready in 3 days. Help please!


Posted at 9:31 p.m. on December 5, 2006

Ram
Ram's gravatar

Hey Ayman,

I would like to know when are you planning to update this module to work in Drupal5.


Posted at 5:21 a.m. on December 7, 2006

Ram
Ram's gravatar

Hi Ayman..,

The portal module is great.. Iam trying to make columns of different widths.. I want to place portlets with large content in one column and portlets with small content into different column.. I tried to change the CSS but could not achieve that.. Is it possible to have columns of different widths?? Please Help me out. Thanks in Advance


Posted at 7:18 a.m. on December 12, 2006

Anonymous
Anonymous's gravatar

Hi Ayman,

first many thanks for this great tutorial.

I updated script.aculo.us to v.1.6.5 und now i'm just getting an Error: "Element.childrenWithClassName is not a function"

Can you fix this, please?

I thik, a Fix may be necessary, v1.7 (still beta) will coming up with nice features.

Best Regards

ps. sorry for my english, i'm not a native speaker. :)


Posted at 4:41 a.m. on January 2, 2007

graham
graham's gravatar

Is there an easy/natural way to join this with the mysite module?

Fantastic work, btw.


Posted at 12:57 p.m. on January 4, 2007

Jon
Jon's gravatar

Awesome post.

Had it working but then upgraded scriptaculous and now I get the same error as a post above "Element.childrenWithClassName is not a function"

Any ideas?

-Jon


Posted at 3:05 p.m. on January 16, 2007

Jon
Jon's gravatar

I fixed the problem, kind of.

I downloaded the prototype.js / scriptaculous.js / dragdrop.js / effects.js file you use on your demo and it works fine now - so there is something with the latest version that is a little incompatible with your toggle and the block list appearing code.

I have a real question actually 2 -

Is there a way to get the div ID of the block you just dropped? I also noticed that the onUpdate doesn't get called when I change the order of blocks in the same column / container. Is this correct as well?

Again, This is fantastic ! Thanks for posting this.

-Jon


Posted at 3:39 p.m. on January 16, 2007

Tom
Tom's gravatar

Hi I was wondering if there was a way to fix that jittering effect you get while your trying to swap panels. I noticed that when I try to change the order of the panels in the Block List the panels swap back and forth very fast while your draging over them. Is there anyway to fix this so that it has a smooth transition? It is a little jarring to the user.

I noticed that it does that sometimes when your trying to swap panels in the columns as well. They flicker back and forth until you drop it into place. When you are dragging something over another panel, they switch places very fast while moving the mouse. It looks amateuristic and would like to know if you can fix that or is it something done behind the scenes in the lib?

Thanks.


Posted at 10:04 p.m. on January 26, 2007

Anonymous
Anonymous's gravatar

How do I use the Module>?>


Posted at 6:02 p.m. on January 29, 2007

Vader
Vader's gravatar

First of all, thanks for the great article!

I'm having problems with the call Element.childrenWithClassName, FireFox just returns an error. Is there anyway around this?


Posted at 9:49 a.m. on February 2, 2007

Vader
Vader's gravatar

As an answer to my self: var content = document.getElementsByClassName('content', block)[0]; this will do the same trick.


Posted at 10:25 a.m. on February 2, 2007

Jimmy Page
Jimmy Page's gravatar

I have the same problem too. Tried it both in IE and Firefox .


Posted at 4:18 p.m. on February 7, 2007

bo
bo's gravatar

hi,

i managed to make the portlets in my jetspeed 2 portal movable by drag with the help of this article. now i would like to know how to save the position of the portlets. any idea? would be great if anyone could help me....

regards, bo


Posted at 6:41 p.m. on February 18, 2007

redwall_hp
redwall_hp's gravatar

I'm trying to create a backend for the portal without using Drupal or anything like that. Okay, the portal.js sends an AJAX query over to save.php. How do i process the query so it's in a form that can be stored in a MySQL database? The code in your blog post didn't work. It gives errors about nonexistant functions. I can't figure out what to do on my own either. I'd appreciate hep if anyone knows what to do. I basically need some php source to put in save.php that will process the query and insert it into a database table (same fields as example in blog post: user,countainer,blocks I beleive).


Posted at 5:18 p.m. on February 19, 2007

Quote Maniac
Quote Maniac's gravatar

I've been thinking of migrating to drupal for my site for a long time. It's currently using Wordpress but after looking at the awesome and smooth effect you create, I'm now seriously going to consider migrating.


Posted at 4:58 a.m. on March 21, 2007

kirill
kirill's gravatar

Hi, great post, thank you for sharing!

However I wonder if anyone noticed a bug in IE that happens in the following scenario.

1) drag Calendar into "Column 1" 2) drag deli.cio.us into "Column 2" 3) drag Google News into "Column 0". This causes "Column 2" with its contents to jump underneath "Column 1".

Thoughts?

Thanks, kirill


Posted at 6:24 p.m. on March 21, 2007

dustin
dustin's gravatar

Some people mentioned a problem with saving rearranged columns and also saving empty columns.

I integrated this app with ruby on rails and had to make sure to write a blank array [] when the column was empty... without doing that, it wouldn't work... After I did that, it worked fine.

Thanks Ayman for such a wonderful app! Dustin


Posted at 6:40 p.m. on April 3, 2007

Geoff
Geoff's gravatar

Hi,

I am have some trouble loading the users setting from the cookie. I am storing the column and box order in a cookie and then on page load I'm writing the divs to the page and then getting the content for the boxes via an AJAX call. Everything is working fine, but the divs that are writing to the page are not sortable. The hide/show toggle works, and all the same classes are applied to the divs as are applied to the divs in the block-list div so I'm confused. I'd love some help with this.

Cheers, Geoff


Posted at 3:49 a.m. on April 27, 2007

Tiziano
Tiziano's gravatar

Hi!

Above all thank you for such an interesting article! I'm trying to build a customized page with widgets in Drupal and I found it very useful. Now, I have a question for you: it seems that the onUpdate event is fired only if a draggable element is moved from a sortable container to another one. Is it possible to fire it also when the user changes the order of the elements inside the same container? I can't manage to save elements order if the user changes it (within the same container).

Thank you again. Tiziano


Posted at 1:45 p.m. on May 14, 2007

Kevin
Kevin's gravatar

This is exactly what I was looking for for one of my projects! The only problem is that the performance seems to be lacking as you drag more and more content into the portal. I have tested with about 80 items and there is some serious lag in dragging. I tested out the iGoogle personalized homepage after adding about 80 items to the page and the dragging worked without any lag. Unfortunately I need to support around 80 items on my site and won't be able to use this because of the drag lag. Keep in mind that I am using a brand new Intel Core2 Duo based computer, so the hardware is not holding it back any. Other than the speed, this is a really nice portal interface! Any help with speeding it up would be great. Kevin


Posted at 11:22 p.m. on May 16, 2007

Alfred
Alfred's gravatar

Same problem here...


Posted at 2:05 p.m. on June 4, 2007

Andy Mortland
Andy Mortland's gravatar

Ayman - this is awesome - exactly what we need for our application. However, we are using the 1.7 version of scriptaculous, in which the 'childrenWithClassName' method has been removed. As this is a crucial component of your script, I wondered if you had ever reworked the script to work with the 1.7 libraries.

I've been trying to make do by altering portal.js, and by changing the method calls to use the getElementsByClassName, but have had little success.

Any pointers would be much appreciated. Thanks!


Posted at 5:01 p.m. on June 5, 2007

Doug
Doug's gravatar

Hello, I am using the YUI Grid template to structure layouts for a portal framework. The goal is to create a portal in which users can define his/her own layout. An example for such portal could be iGoogle. But iGoogle or most available portal out there have very limited layout structures. For example iGoogle only allows you to create 2 or 3 columns portal layout. What if I want something like a one-column layout on top of 3 -column layout?

Inspired your article, I have the idea of combining what proposed in this article with the YUI grid framework to create a very flexible portal layout.

Please see it for yourself at

http://www2.cs.uh.edu/~dmly/portal/layoutBuilder.html

After creating your own portal layout click on "get This Layout" button to go to the next screen with your layout built and a set of demo portlet for you to drag and drop to the layout to see how it work.

I test this code with Firefox 2.0 and the latest version of IE (7.0) Please let me know if you have any suggestion. I would like to make it an extensible framework, probably in Ruby on Rails first as a proof of concept.

Regards,

Doug


Posted at 1:28 a.m. on June 23, 2007

Barker
Barker's gravatar

I've recently been playing around with your example, and having upgraded to the latest version of prototype (v1.5.1), which also requires you to upgrade script.aculo.us (to v1.7.1 b3).

I see that Element.childrenWithClassName has been removed from the library and you'll need to replace the two lines that use it with Element.getElementsByClassName.

Excellent tutorial, it's been very useful, Cheers


Posted at 10:22 a.m. on July 6, 2007

Tom Jenkins
Tom Jenkins's gravatar

Hi there

I have used and implemented the above code and it works perfectly. I've had to change the database delete / insert / select into coldfusion and a few other bits and bobs but it still works really nicely. One problem I have though is saving the state of change within a single column.

For example:

If you drag the calendar into column0 then drag who's online into column0 BELOW the calendar, this work perfectly. If i then place the who's online ABOVE the calendar the onUpdate is not triggered meaning this ordering change is not saved to the database.

How would i make it so you can re order items in the same column?

Thank you for help in advance

Tom Jenkins


Posted at 1:39 p.m. on July 9, 2007

Tom Jenkins
Tom Jenkins's gravatar

Hi there

I posted here yesterday but nothing has come of it as yet. I really need help with the question that i submitted. Please help.

Kind Regards

Tom Jenkins


Posted at 8:27 a.m. on July 10, 2007

Adam
Adam's gravatar

I wrote a solution to the childrenWithClassName issue. Im currently using scriptaculous 1.7.1_beta3 with prototype 1.5.1.1 and everything works great.

In the portal.js file replace:

var content = Element.childrenWithClassName( block, this.options.content, true );

   var toggle = Element.childrenWithClassName(
      block, this.options.toggle, true
    );

WITH:

var content = block.getElementsByClassName(this.options.content)[0]; var toggle = block.getElementsByClassName(this.options.toggle)[0];


Posted at 4:38 p.m. on July 10, 2007

Anonymous
Anonymous's gravatar

Ella,

After removing the hyphens from all of the div IDs, I'm now receving block1 is not defined. I too was getting the Error: Expected ':' before. How did you precent the not defined issue?


Posted at 1:15 p.m. on October 10, 2007

Devender Dagar
Devender Dagar's gravatar

Hi Sami

Did you find some solution for cookie save ? I am also looking for same functionality and working on this for the last one week. Do you have some idea for doing this ?

I am very good in coding as well as in analysis but really I have no idea for doing this. I am trying some options. Let see how much time it takes?

If you have some logic please let me know so that I can implement it.


Posted at 11:47 a.m. on June 18, 2008

Anonymous
Anonymous's gravatar

Hi Ayman,

Thanks for this great module. I have got a bit problem. I want to use this module to make a similar system like drag and drop shopping cart. I mean i want one column to be fixed with certain contents which will always be draggable only and another column which is always droppable. I want the content from the draggable area to be dropped to the droppable area keeping the content of the draggable area as it is along with scrolling the page as draggable is dragged.

In Scriptaculous module, for this there is an option of revert: true and for scroll: scroll: window. I am tryig to use it in this module, but isn't getting any idea how to use it.

I tried by adding revert and scroll in the following part:

new Draggable(this.options.blocklist, { handle: this.options.blocklisthandle,
revert: true, scroll: window,

  }
);

in portal.js file, but it doesn't works,

Can you give any help or suggestions regarding this problem.

Thanks


Posted at 12:32 p.m. on July 7, 2008

Chanchal Gandhwani
Chanchal Gandhwani's gravatar

Hi Ayman,

Thanks for this great stuff. I have a question can you please suggest how can i add a ondrop event handler

Thanks


Posted at 5:56 p.m. on July 15, 2008

dflorence
dflorence's gravatar

the code $_POST['container']); should be $_POST['value]);, as the .js script sets the name of the data posted to "value."'


Posted at 8:13 p.m. on September 4, 2008

Cabada
Cabada's gravatar

Where should i put the loading code?


Posted at 8:48 a.m. on October 5, 2008

Cabada
Cabada's gravatar

Can i see that source?


Posted at 7:14 p.m. on October 5, 2008

Cabada
Cabada's gravatar

How can i make that one window go just to the 2nd and 3rd column, and not letting it going to the first column?

HELP PLEASE!


Posted at noon on October 6, 2008

Mr. Low
Mr. Low's gravatar

This code is not working with Internet explorer; is when the user loads the interface from the DataBase

for (var container in settings) { settings[container].each(function (block) { $(container).appendChild($(block)); });

Please, answer me.


Posted at 7:14 a.m. on October 7, 2008

Bronson
Bronson's gravatar

Great stuff y've designed.

My question is how can'i limit the number of item in backend.

Ie, if front end carry 10 items backend is not eligible for more than 5 items, i can't figure out how to set this upright (in meantime trigger alert popup window to advise the visitor of that fact when trying to exceed this limit.)

Help would vrey meuch appreciated..

Rgds


Posted at 8:50 a.m. on October 29, 2008

anon
anon's gravatar

i like your tutorial many thanks but i am using this script outside drupal, and i was wondering if you could help me write a cookie to remember the block position? many thanks


Posted at 11:21 p.m. on November 22, 2008

RJ
RJ's gravatar

When I tried to get this running with the latest Scriptaculous 1.8.2 and Prototype 1.6.0.3, I found that different methods of Class, Element, Sortable, Draggable, and so on, require the classes represented in different ways. (IDs of elements don't even require a '#' to be identified, which is also inconsistent in my opinion, but it's off-topic in this context.) What I mean here is that, at some places I need to prefix the class name from this.options with '.', and for others I don't need to. Otherwise, the $$(...) and $(...) return null or nothing at all.

I'd hope in the future versions IDs would be represented with prefix '#', Classes with a prefix '.', and HTML tags with no prefixes.


Posted at 7:30 p.m. on January 8, 2009

RJ
RJ's gravatar

Oh by the way, great tutorial and thank you so much! :)


Posted at 7:31 p.m. on January 8, 2009

Rich
Rich's gravatar

Thanks for the tutorial Ayman, I've got most of this all working fine after converting to coldfusion but I'm having some issues with the load part of it. When I pull back the data from my query I get the following error message:

block1 is not defined

I was having some issues with the container being referenced as well but saw that after dropping the hyphens and renaming columns to portalcolumn0 etc that worked but would appreciate any help from someone who can put me on the right track for getting this last part up and running.


Posted at 2:07 p.m. on February 6, 2009

Rich
Rich's gravatar

Put "" characters around each block element and it works! Fantastic.


Posted at 10:05 a.m. on February 10, 2009

Matty B
Matty B's gravatar

This is a great script, however what if you wanted a block to span all columns? how would this be achieved.


Posted at 1:35 p.m. on March 6, 2009

Michel
Michel's gravatar

Hi,

I noticed the script was not working with the newest prototype, but it was exactly what i needed so i kindof recoded your script to the newest prototype. And i added some extra features to it like a 'config' option for the block and delete button.

For those interested:

/** Drag&Drop; Portal interface with Prototype and Scriptaculous, Optimized for prototyime 1.6.0.3, by Michel (michelhiemstra.nl), original by: http://aymanh.com/drag-drop-portal-interface-with-scriptaculous

    Usage:

        <script type="text/javascript">
            var settings = { 'portal-column-0':['block-aggregator-feed-1'] };
            var options = { editorEnabled : true };
            var portal;

            Event.observe(window, 'load', function() {
                portal = new Portal(settings, options);
            }, false);
        </script>
<div id="content">
<div id="portal">
<div class="portal-column dir-horizontal" id="portal-column-0"></div>
<div class="portal-column dir-vertical" id="portal-column-1"></div>
</div>
<div class="portal-column" id="portal-column-block-list">
<h2 class="block-list-handle">Block List</h2>
<div class="block block-test" id="block-test">
<h3 class="handle"><div class="block-controls" style="display: none;"><a class="block-remove"><span>x</span></a> <a class="block-config"><span>e</span></a></div>Testblok</h3>
<div class="config" style="display: none;">
<div>config-params</div>
<div align="right">
<a href="#" class="cancel-button">cancel</a>
<a href="#" class="save-button">cancel</a>
</div>
</div>
<div class="content">
<div id="block-test-content">
                            test
                        </div>
</div>
</div>
</div>
</div>

**/

var Portal = Class.create();

Portal.prototype = {

    initialize : function (settings, options, data) {
        // set options
        this.setOptions(options);

        // set blocks to their positions
        this.applySettings(settings);

        // if editor is enabled we proceed
        if (!this.options.editorEnabled) return;

        // get all available columns
        var columns = $(this.options.portal).getElementsByClassName(this.options.column);

        // loop trough columns array
        $A(columns).each(function(column) {

            // create sortable
            Sortable.create(column, {
                containment : $A(columns),
                constraint  : false,
                tag         : 'div',
                only        : this.options.block,
                dropOnEmpty : true,
                handle      : this.options.handle,
                hoverclass  : this.options.hoverclass,

                onUpdate    : function (container) {

                    // if we dont have a save url we dont update
                    if (!this.options.saveurl) return;

                    // if we are in the same container we do nothing
                    if (container.id == this.options.blocklist) return;

                    // get blocks in this container
                    var blocks = container.getElementsByClassName(this.options.block);

                    // serialize all blocks in this container
                    var postBody = container.id + ':';
                    postBody += $A(blocks).pluck('id').join(',');
                    postBody = 'value=' + escape(postBody);

                    // save it to the database
                    new Ajax.Request(this.options.saveurl, { method: 'post', postBody: postBody });

                }.bind(this)
            });

        }.bind(this));

        //-----------//

        // get all blocks
        var blocks = $(this.options.portal).getElementsByClassName(this.options.block);

        // loop trough blocks
        $A(blocks).each(function(block) {

            // enable controls if available
            if (typeof(block.getElementsByClassName('block-controls')[0]) == 'object') {
                block.getElementsByClassName('block-controls')[0].setStyle({'display' : 'block'});
            }

            // detail, set cursor style to move when in admin modus
            if (typeof(block.getElementsByClassName('handle')[0]) == 'object') {
                block.getElementsByClassName('handle')[0].setStyle({'cursor' : 'move'});
            }

            // toggle configuration element
            if (typeof(block.getElementsByClassName(this.options.config)[0]) == 'object') {
                Event.observe(block.getElementsByClassName(this.options.config)[0], 'click', function () {
                    block.getElementsByClassName(this.options.configElement)[0].toggle();
                }.bind(this));
            }

            // observe save button
            if (typeof(block.getElementsByClassName(this.options.configSave)[0]) == 'object') {
                Event.observe(block.getElementsByClassName(this.options.configSave)[0], 'click', function (e) {
                    alert('save');
                }.bind(this));
            }

            // observe cancel button
            if (typeof(block.getElementsByClassName(this.options.configCancel)[0]) == 'object') {
                Event.observe(block.getElementsByClassName(this.options.configCancel)[0], 'click', function (e) {
                    block.getElementsByClassName(this.options.configElement)[0].toggle();
                }.bind(this));
            }

            // observe delete block button
            if (typeof(block.getElementsByClassName(this.options.remove)[0]) == 'object') {
                Event.observe(block.getElementsByClassName(this.options.remove)[0], 'click', function (e) {
                    if (confirm('Are you sure you wish to delete this block?')) {
                        new Ajax.Request(_this.options.saveurl + '/delete/', { method: 'post', postBody: 'block='+block.id }); $(block.id).hide();
                    }

                }.bind(this));
            }

        }.bind(this));

    },

    applySettings : function (settings) {
        // apply settings to the array
        for (var container in settings) {
            settings[container].each(function (block) { $(container).appendChild($(block)); });
        }
    },

    setOptions : function (options) {
        // set options
        this.options = {
            editorEnabled   : false,
            portal          : 'portal',
            column          : 'portal-column',
            block           : 'block',
            content         : 'content',
            configElement   : 'config',
            configSave      : 'save-button',
            configCancel    : 'cancel-button',
            handle          : 'handle',
            hoverclass      : 'block-hover',
            remove          : 'block-remove',
            config          : 'block-config',
            blocklist       : 'portal-column-block-list',
            blocklistlink   : 'portal-block-list-link',
            blocklisthandle : 'block-list-handle',
            saveurl         : '/saveurl'
        }

        Object.extend(this.options, options || {});
    },

    loadData : function (block, type, data_id) {
        // load data CODE ME
        new Ajax.Updater(block + '-content', '/congresses.'+type+'/data='+data_id);
    }

};

Its not optimal, stil working on it to get the data input to load the blocks dynamicly


Posted at 8:46 a.m. on March 10, 2009

Stella
Stella's gravatar

Nice tutorial Ayman! Thanks for posting it here. However, I have a query before I switch some of my sites to drupal. is it possible to shift or transfer wordpress blogs (self-hosted) to drupal without losing any data?

Waiting for your reply!

Stella.


Posted at 1:49 a.m. on March 19, 2009

Art
Art's gravatar

Will it work with drupal 6? Are you planning in having a theme like that for drupal, or wordpress that would be great, we will be able to make sites lik the bbcs one I think they copied your idea http://www.bbc.co.uk/


Posted at 12:36 a.m. on April 3, 2009

Flora
Flora's gravatar

i like your tutorial many thanks but i am using this script outside drupal, and i was wondering if you could help me write a cookie to remember the block position? many thanks Flora


Posted at 1:50 p.m. on April 27, 2009

Paul
Paul's gravatar

Great sample. is there a way where one could save the settings so that if i login back i can get the settings?

Thanks Paul


Posted at 7:25 p.m. on May 27, 2009

Paul
Paul's gravatar

Hi

I am not using Drupal. I am using a JSP running on Apache . Is there a way to store/restore the user settings?

Thanks P


Posted at 6:44 p.m. on May 28, 2009

remiglobal
remiglobal's gravatar

i think, somebody else has written earlier that you have to disable some test @ some lines. but somehow this method is slow.

another method is to detect when you have stopped dragging. upon sensing that you have stopped dragging, you can detect the container and use ayman's method of serializing the widget. If you can code carefully, you will only need to send the data back once.

happy hacking, and thanks to ayman for this great stuff. It took me one whole day to digest the above problem :-)


Posted at 1:55 a.m. on June 1, 2009

Anonymous
Anonymous's gravatar

I am wondering the same thing... Will this work with the recent version of Drupal?


Posted at 4:37 p.m. on June 17, 2009

Paul
Paul's gravatar

I am trying to display the page with a pre-defined settings. For the sample code i am trying to set

var settings = { portal-column-0: [block-archive-0, block-aggregator-feed-2], portal-column-2: [block-user-3] };

but i am not getting the desired results?

Any help?


Posted at 5:06 p.m. on June 17, 2009

Paul
Paul's gravatar

Would it be possible for you to post the source?


Posted at 5:32 p.m. on June 22, 2009

Nirav
Nirav's gravatar

i have got your Example. but i have create this t ype of Application using Asp.net Web Part. Plz help me


Posted at 10:54 a.m. on August 13, 2009

nuwan
nuwan's gravatar

When the block is moved within same column The OnUpdate event is not fired ? any suggestions ?


Posted at 11:54 a.m. on August 27, 2009

Les Dunaway
Les Dunaway's gravatar

Thanks for some insight into Drupal. I'm using WordPress for my personal development blog and I've been curious about Drupal.


Posted at 3:33 p.m. on May 2, 2010

Post a comment

HTML is not allowed. You can use markdown syntax to format your comment.