timkl
timkl

Reputation: 3339

How to achieve this advanced interface with jQuery?

I'm currently fighting a fierce battle with an interface I'm trying to put together. I'm pretty decent at CSS & XHTML, but my jQuery/JavaScript skills are not too impressive. What I'm looking for is not so much a strict answer, but some hints on how to go about these kinds of tasks.

What I am trying to achieve is a layout with some boxes in the main-content area that can be expanded and collapsed when the user press either a link in the sidebar or on a header on the box itself.

dashboard http://www.timkjaerlange.com/foobar/example-dashboard-v01.gif

Furthermore I want the browser to remember what box was expanded last by setting a cookie. If that wasn't enough I also want to be able to add a "limited"-class to each box and thereby disabling it, and it's corresponding sidebar-list-item.

Also I want the box to have a class based on it's state (expanded, collapsed, limited).

This is my markup:

<!-- S I D E B A R - L E F T -->    

<div id="sidebar-left">
    <ul class="sidebar-menu-1">
        <li><strong>List headline</strong>
        </li>
        <li><a href="#">Open module 1</a>
        </li>
        <li><a href="#">Open module 2</a>
        </li>
        <li><a href="#">Open module 3</a>
        </li>
    </ul>

</div><!--/sidebar-left-->


<!-- M A I N - C O N T E N T -->    

<div id="main-content">

    <!-- dashboard-module -->
    <div class="dashboard-module">
    <div class="dashboard-module-content limited">
        <h2>Headline 1</h2>
        <p class="teaser">Teaser-text 1</p>
        <div class="expand-collapse">
            [some content]
    ´   </div>
    </div>
    </div><!--/dashboard-module-->


    <!-- dashboard-module -->
    <div class="dashboard-module">
    <div class="dashboard-module-content limited">
        <h2>Headline 2</h2>
        <p class="teaser">Teaser-text 2</p>
        <div class="expand-collapse">
            [some content]
    ´   </div>
    </div>
    </div><!--/dashboard-module-->


    <!-- dashboard-module -->
    <div class="dashboard-module">
    <div class="dashboard-module-content limited">
        <h2>Headline 3</h2>
        <p class="teaser">Teaser-text 3</p>
        <div class="expand-collapse">
            [some content]
    ´   </div>
    </div>
    </div><!--/dashboard-module-->


</div><!--/main-content-->

And this is my jQuery:

// If the dashboard-module is "limited" do this:

$('div[class*=limited]').each( function(index) {
    var countFix = $('div.dashboard-module-content').index(this);
    var countFixPlusTwo = countFix + 2;
    $( 'ul.sidebar-menu-1 li:nth-child(' + countFixPlusTwo + ')' ).unbind('click').removeClass('selected').addClass('sidebar-menu-locked');
    $(this).parent('.dashboard-module').find('h2').unbind('click');
    $(this).find('.expand-collapse').remove();
    $(this).find('.dashboard-module p:hidden').show();
    $(this).addClass('limited-btn');
});

// Check for cookies and do stuff accordingly:

var cookieTest = $.cookie("cookie");

if(cookieTest == null){
    $('.js-hidden').hide();
    $('ul.sidebar-menu-1 li:not(.sidebar-menu-locked):eq(1)').addClass('selected');
    $('.dashboard-module-content:not(.limited):eq(0)' ).toggleClass('hide-btn');    
    $('.dashboard-module-content:not(.limited):not(:eq(0))' ).removeClass('hide-btn');
    $('.expand-collapse:eq(0)' ).show();
    };

if(cookieTest != null){
    var cookieTestPlusTwo = parseInt(cookieTest, 10) + 2;
    $('.dashboard-module-content:eq(' + cookieTest + ')' ).toggleClass('hide-btn'); 
    $('.dashboard-module-content:not(:eq(' + cookieTest + '))' ).removeClass('hide-btn');   
    $('ul.sidebar-menu-1 li:nth-child(' + cookieTestPlusTwo + '):not(.sidebar-menu-locked)').addClass('selected');
    $('ul.sidebar-menu-1 li:not(:nth-child(' + cookieTestPlusTwo + '))').removeClass('selected');
    $('.expand-collapse:eq(' + cookieTest +  ')' ).show();
    $('.expand-collapse:not(:eq(' + cookieTest +  '))').hide();
    };

// When clicking dashboard-module headers do stuff:

$('.dashboard-module h2:not(.limited h2):not(.tooltip h2)').click(function(index) {

    var indexFoo = $('.dashboard-module h2:not(.tooltip h2)').index(this);
    var indexPlusTwo = indexFoo + 2;
    var indexCookie = indexFoo;

    $.cookie("cookie", indexCookie, { expires: 7 });

    $('.dashboard-module-content:eq(' + indexFoo + ')').toggleClass('hide-btn');    
    $('.dashboard-module-content:not(:eq(' + indexFoo + '))' ).removeClass('hide-btn');

    $('ul.sidebar-menu-1 li:nth-child(' + indexPlusTwo + ')').addClass('selected');
    $('ul.sidebar-menu-1 li:not(:nth-child(' + indexPlusTwo + '))').removeClass('selected');

    $('.dashboard-module-content:eq(' + indexFoo + ') .expand-collapse').toggle('fast');
    $('.dashboard-module-content:not(:eq(' + indexFoo + ')) .expand-collapse').hide('fast');

    $('.dashboard-module-content:not(.limited)').each( function(){
        if($(this).parent('.dashboard-module').find('.hide-btn').length == 0){
            $(this).find('p.teaser').show();
        }
        else {
            $(this).find('p.teaser').hide();
        }
    });
});

// When clicking sidebar list-items do stuff:

$('.sidebar-menu-1 li:not(.sidebar-menu-locked)').click(function(index) {

    var indexFoo = ($('.sidebar-menu-1 li').index(this) - 1);
    var indexPlusTwo = indexFoo + 2;
    var indexCookie = indexFoo;

    $.cookie("cookie", indexCookie, { expires: 7 });

    $('.dashboard-module-content:eq(' + indexFoo + ')').toggleClass('hide-btn');    
    $('.dashboard-module-content:not(:eq(' + indexFoo + '))' ).removeClass('hide-btn'); 

    $('ul.sidebar-menu-1 li:nth-child(' + indexPlusTwo + ')').addClass('selected');
    $('ul.sidebar-menu-1 li:not(:nth-child(' + indexPlusTwo + '))').removeClass('selected');

    $('.dashboard-module-content:eq(' + indexFoo + ') .expand-collapse').toggle('fast');
    $('.dashboard-module-content:not(:eq(' + indexFoo + ')) .expand-collapse').hide('fast');

    $('.dashboard-module-content:not(.limited)').each( function(){
        if($(this).parent('.dashboard-module:not(.limited)').find('.hide-btn').length == 0){
            $(this).find('p.teaser').show();
        }
        else {
            $(this).find('p.teaser').hide();
        }
    });

});

As for now it all works in FF, but in IE7+8 it doesn't. But my question is not so much about how to make it work in IE, but more on how you would go about this kind of task? I suspect that my jQuery is not exactly DRY, but how do I condense it?

How would you write and organize your code? Sorry if this is a too broad and long-winded question, but I'm really interested in how experienced jQuery designers go about challenges like this one.

Update: Thanks to Tatu Ulmanen's help I've managed to put together this: http://timkjaerlange.com/foobar/jquery-test/index.html

Upvotes: 1

Views: 900

Answers (3)

Mottie
Mottie

Reputation: 86473

I think Tatu posted a wonderful answer. But I worked on some scripting to more closely work with your HTML. I posted a working demo for you here... the extra "} }) " on the right edge only shows up in the bin for some reason (I don't know how to get rid of it).

CSS

#sidebar-left {
 float:left;
 width: 200px;
 height: 700px;
 background: #444;
 padding: 20px;
}
#main-content {
 float:left;
 width: 600px;
 height: 700px;
 background: #555;
 padding: 20px;
}
.sidebar-menu-1 .selected a { color: #080; }
.sidebar-menu-1 .sidebar-menu-locked a { color: #d00; text-decoration: line-through; }
.dashboard-module h2 {  cursor: pointer; }
.dashboard-module { border: #777 1px solid; padding: 5px 15px; margin-bottom: 5px; }
.dashboard-module-content {}
.limited h2 { text-decoration: line-through; }

.expand-collapse { display: none; }

.show_btn { background: #08c url(http://i46.tinypic.com/2vd3246.gif) 98% 4px no-repeat; }
.hide_btn { background: #080 url(http://i47.tinypic.com/iqadz5.gif) 98% 4px no-repeat; }
.limited_btn { background: #d00 url(http://i47.tinypic.com/209ghzp.gif) 98% 4px no-repeat; }

HTML (only slightly modified from yours)

<!-- S I D E B A R - L E F T -->

<div id="sidebar-left">
 <ul class="sidebar-menu-1">
  <li><strong>List headline</strong></li>
  <li><a href="#m1">Open module 1</a></li>
  <li><a href="#m2">Open module 2</a></li>
  <li><a href="#m3">Open module 3</a></li>
  <li><a href="#m4">Open module 4</a></li>
 </ul>
</div><!--/sidebar-left-->

<!-- M A I N - C O N T E N T -->

<div id="main-content">

    <!-- dashboard-module -->
    <div id="m1" class="dashboard-module">
    <div class="dashboard-module-content">
        <h2>Headline 1</h2>
        <p class="teaser">Teaser-text 1</p>
        <div class="expand-collapse">
                [some content]
        </div>
    </div>
    </div><!--/dashboard-module-->


    <!-- dashboard-module -->
    <div id="m2" class="dashboard-module">
    <div class="dashboard-module-content">
        <h2>Headline 2</h2>
        <p class="teaser">Teaser-text 2</p>
        <div class="expand-collapse">
                [some content]
        </div>
    </div>
    </div><!--/dashboard-module-->


    <!-- dashboard-module -->
    <div id="m3" class="dashboard-module">
    <div class="dashboard-module-content">
        <h2>Headline 3</h2>
        <p class="teaser">Teaser-text 3</p>
        <div class="expand-collapse">
                [some content]
        </div>
    </div>
    </div><!--/dashboard-module-->

    <!-- dashboard-module -->
    <div id="m4" class="dashboard-module">
    <div class="dashboard-module-content limited">
        <h2>Headline 4</h2>
        <p class="teaser">Teaser-text 4</p>
        <div class="expand-collapse">
                [some content]
        </div>
    </div>
    </div><!--/dashboard-module-->

</div><!--/main-content-->

Script

$(document).ready(function(){
 $('.dashboard-module').each(function(){
  var mod = $(this);
  mod.find('h2').addClass('show_btn'); 
  // If the dashboard-module is "limited" do this:
  if (mod.find('.dashboard-module-content').hasClass('limited')){
   $('.sidebar-menu-1').find('a[href$=' + mod.attr('id') + ']').parent().removeClass('selected').addClass('sidebar-menu-locked');
   mod.find('.expand-collapse').remove();
   mod.find('p:hidden').show();
   mod.find('h2').removeClass('show_btn hide_btn').addClass('limited_btn');
  };
 // Add click to Dashboard Modules
  mod.find('h2').click(function(){
   toggleContent(mod);
  })
 })
 // Add click to Side Menu
 $('.sidebar-menu-1 a').click(function(){
  // The dashboard-module ID is contained in the sidebar-menu link href attribute
  toggleContent( $( $(this).attr('href')) );
  return false;
 })
 function toggleContent(mod){
  // don't open limited content
  if (mod.find('.dashboard-module-content').hasClass('limited')) return false;
  // close open content
  if (!mod.find('h2').hasClass('hide_btn')){
   $('.hide_btn').removeClass('hide_btn').addClass('show_btn').parent().find('.expand-collapse').hide();
   $('.sidebar-menu-1 .selected').removeClass('selected');
  }
  var btn = (mod.find('.expand-collapse').toggle().is(':hidden')) ? 'show_btn' : 'hide_btn';
  mod.find('h2').removeClass('show_btn hide_btn').addClass(btn);
  $('.sidebar-menu-1').find('a[href$=' + mod.attr('id') + ']').parent().toggleClass('selected');
 }
})

Upvotes: 0

tony-p-lee
tony-p-lee

Reputation: 807

I suggest start with http://plugins.jquery.com/.

Look over the plugins example there. Play the demo, document and review the source code.

Upvotes: 1

Tatu Ulmanen
Tatu Ulmanen

Reputation: 124878

Opening modules

For the sidebar links -> content relation, use rel tags in sidebar that correspond to the main content's divs' ID's, like this:

<ul id="sidebar">
    <li><a href="#" rel="tab_1">Open tab 1</a></li>
    <li><a href="#" rel="tab_2">Open tab 2</a></li>
    <li><a href="#" rel="tab_3">Open tab 3</a></li>
</ul>

...

<div id="content">
    <div class="module" id="tab_1"> ... </div>
    <div class="module" id="tab_2"> ... </div>
    <div class="module" id="tab_3"> ... </div>
</div>

And then in jQuery, the change of tab is easy to implement:

$('#sidebar a').click(function() {
    $('#content div.module').hide();
    $('#content div#'+$(this).attr('rel')).show();
    return false;
});

Module headers

If you want to have the headers of the modules to be visible always, you can do something like this:

<div id="content">
    <div class="module" id="tab_1">
        <h2>Module header</h2>
        <p class="module_tools">
            <a href="#" class="module_hide">Hide</a>
            <a href="#" class="module_show">Show</a>
        </p>
        <div class="module_contents">
            ...

And in jQuery:

$('#sidebar a').click(function() {
    var $visible_module = $('#content div#'+$(this).attr('rel'));

    $('#content div.module div.module_contents').hide();
    $('#content div.module div.module_contents a.module_hide').hide();
    $('#content div.module div.module_contents a.module_show').show();

    $visible_module.find('div.module_contents').show();
    $visible_module.find('a.module_hide').show();
    $visible_module.find('a.module_show').hide();

    return false;
});

Module show and hide

Then, to implement the show buttons in modules, I would 'cheat' and have them press the corresponding button in sidebar.

$('#content .module_show').click(function() {
    $('#sidebar a[rel='+$(this).closest('div.module').attr('id')).click();
    return false;
});

The hide button is easy to implement:

$('#content .module_hide').click(function() {
    $(this).closest('div.module').find('div.module_contents').hide();
    return false;
});

Locked modules

And last thing is to implement the 'locked' feature. For that, I would just add a class to the sidebar's links that tells the current tab is inactive and then modify the modules according to that:

<ul id="sidebar">
    <li><a href="#" rel="tab_1">Open tab 1</a></li>
    <li><a href="#" rel="tab_2" class="inactive">Open tab 2</a></li>

And in jQuery:

$('#sidebar a.inactive').each(function() {
    var $target_module = $('#content div#'+$(this).attr('rel'));
    // Remove the links alltogether
    $target_module.find('.module_hide, .module_show').remove();
    return false;        
});

And the sidebar's links' click event must be modified also:

$('#sidebar a').click(function() {
    if($(this).hasClass('inactive')) return false;
    ...

That covers all the bases not including the cookie save. Hope this gives you some pointers on how you can simplify your code. Note that I haven't tested any of this, so I cannot guarantee it works. Feel free to ask if you don't understand something.

Be sure to use CSS styling to set the initial state of your application, for example hiding all the 'hide' links and .module_contents from modules, as they probably are closed by default.

Upvotes: 4

Related Questions