Showing posts with label jquery. Show all posts
Showing posts with label jquery. Show all posts

Friday, August 20, 2010

Restoring Browser Functions to Clickable DIVs Without Javascript

Occasionally the situation comes up in web development where you have a highly styled tag such as
<div> or <li> that you want to make clickable.  This often happens when you have a list of items in which you want to make the entire item clickble instead of just the title for example.  The standard fare for doing so is to set { cursor: pointer; } with CSS then to bind the .click event to the tag (in jQuery as so)....

$('#myDivTag').click(function() {
    // your code here
});

Don't get me wrong -- this does work.  However, in doing this what you loose is the standard functionality that most viewers are used to; things like middle clicking to open in a new tab, right-click to copy URL address, and so on.  There is also the lag between when the page renders visibly to the user but the javascript hasn't downloaded and $(document).ready hasn't yet fired.  In this case the tag will not be clickable but the mouse cursor will indicate that it should be.

I've been dealing with this more so lately and there are a couple of ways to solve this problem.

The first is to wrap the <div> (or li or whatever else) tag in an anchor tag <a> and set that anchor tag to { display: block; }.

<style type="text/css">
   #myDivWrapper { display: block; }
   #myDiv {  }
</style>

<a id="myDivWrapper">
    <div id="myDiv">
       ... content ...
    </div>
</a>

By wrapping the div tag in an anchor you've restored the default functionality viewers are used to with a clickable item.  This works in most browsers but for the most part things like <div> and <li> are not allowed inside of an anchor tag per HTML language specifications.  In a sense in doing something like this you are rolling the dice that the next version of IE9 strict mode won't render when it hits this.

It also feels kind of  weird, because all of the styling that was on #myDiv, things like height, width, float, etc., now need to be applied to the anchor tag instead.  And no one likes wrappers. They just clutter things up.

The 2nd way to solve this problem which I've started using lately is to have an absolutely positioned anchor overlay the area of the div.  Consider the following...

<style type="text/css">
    #myDiv { position: relative; }
    a.clickable-overlay {
        position: absolute;
        top: 0;
        left: 0;
        height: 100%;
        width: 100%;
        z-index: 1;
        display: block;
        background-color: transparent;
    }
</style>

<div id="myDiv">
    ... content ...
    <a class="clickable-overlay" href="#wherever"></a>
</div>

Here's what's happening:  At the end of the <div> (or li or whatever) we're adding an anchor tag.  The parent div is set to { position: relative }, and the anchor tag to { position: absolute; }.  If you're not familiar with position absolute you're missing out.  The way it is designed to work is that any element positioned absolutely is done so in relation to it's parent (or any parent's parent) that is positioned either relatively or absolutely.  What this means is that in setting our <div> tag to position: relative the anchor tag with position absolute will be positioned relative to the div.  Top 0 and left 0 will be the top left of the div,  and height 100%, width 100% will cause the anchor to fill out the area of the div.

The result is that we are left with a transparent anchor tag that completely overlays the area of the div, providing the default clickable behavior users expect.  The last CSS trick to making this work is to set z-index to 1, forcing the browser to always render the anchor tag over whatever content is in the div.

The positives to this method are that 1. we restore the expected clickable behavior without javascript (meaning it also works as soon as that markup is rendered and not when $(document).ready is fired) and 2. we're using correct HTML syntax.

I should point out there is a downside to this method, in that because the anchor overlays the div, the contents of the div are no longer selectable or clickable.  Depending on your application this may be negligible.

Friday, December 18, 2009

Where Have All The Good DatePickers Gone?

Is it just me, or is it hard to find a good jQuery-based date picker? Or maybe I'm just picky.

The widget needs to support selecting a date range (there goes most of them). And it needs to be able to support configuring a starting and ending time. And it has to support Themeroller (there goes whatever few remained).

So as it seems is starting to become a norm for me, I'm working on a custom jQuery widget. It's been provisionally named "LoneRangeSelector". It's for a custom web-app-calendar sort of thing.

Provisionally, here's my progress thus far...


The main goal is to make an interface that's quick and easy to setup the date-related parameters of an event, without having to enter 4 different fields (start date, start time, end date, end time).

The secondary goal is that as a byproduct of having a single widget, there is quite a bit less error checking that needs to be done (ie. it's impossible to type in an ending date that occurs before the starting date).

I actually re-created the calendar code instead of trying to modify jQuery UI's datepicker, though it does implement identical css classes so the Themeroller appearance is the same.

The lower portion has been one of the more challenging. I think the time slider needs to have some type of time indication on it, because just at first glance there's no way you konw that it spans midnight to midnight.

Concept:

Friday, December 11, 2009

Using Google's jQuery CDN with Django when not in development

I wanted to transition the Django site I work with primarily to use Google's jQuery CDN. However, when developing locally, it's often faster to just use a local copy. What I wanted was a way to toggle which copy of jQuery was being used based on the environment.

Environment Detection
Before we can toggle the jQuery location, we need to have a way to detect which environment we're running in.I previously blogged about how I have my settings.py configured. There are different ways to do this. One is to use local_settings.py, the other is to have conditional code in your main settings.py which determines values. Either way works.

As for my setting, on the webserver is a folder structure that resembles...

    \webroot
\django_devel
\django_prod
My webserver pulls double duty, hosting both a production version ("prod") and a staging/testing version ("devel"). In my settings.py file I'm using this to determine which environment Django is running in...
import os
DJANGO_ROOT = os.path.abspath(os.path.dirname(__file__))

#
# Globals for determining settings
#
STAGING = PRODUCTION = DEVELOPMENT = False

if 'django_devel' in DJANGO_ROOT:
STAGING = True
elif 'django_prod' in DJANGO_ROOT:
PRODUCTION = True
else:
DEVELOPMENT = True

Most likely, you'll have some other what you're determining your environment. In that case just substitute that.

{% jquery template tag %}
Now that we have the required support code to detect what environment we're running in, putting together the actual implementation is simple.

I decided to go with a template tag. It's simple and easy.
import settings
from django import template
from django.conf import settings

register = template.Library()


# -----------------------------------------------------------------------------
# jQuery
# -----------------------------------------------------------------------------
@register.tag
def jquery(parser, token):
return JQueryNode()

class JQueryNode(template.Node):
def render(self, context):
jquery = 'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js'
jquery_ui = 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js'
if getattr(settings, 'DEVELOPMENT', True):
media_url = getattr(settings, 'MEDIA_URL', '/media/')
jquery = '%sjs/jquery-1.3.2.min.js' % media_url
jquery_ui = '%sjs/jquery-ui-1.7.2.custom.min.js' % media_url
return '<script type="text/javascript" src="%s"></script><script type="text/javascript" src="%s"></script>' % (jquery, jquery_ui)

There are a couple of things to note here.

First, there's a hard-coded path in this code. Because I plan to always use this template tag every time I need jQuery, I felt confident doing so as it leaves only one place to edit. However, if you won't be following that rigid of an implementation pulling the locations of jQuery out and into a settings.py file would probably be a smart idea.

Second, I'm implementing my type of detection for the environment. This probably differs from the majority of Django users.

When all is said and done, all that needs to remain is to simply drop the tag into base.html and we're good to go.
{% load jquery %}{% jquery %}

Friday, November 20, 2009

Using jQuery's Dialog Function to Get User Input

The jQuery UI library provides a really useful method: dialog. It allows any tag to be rendered as a fake "window" in the browser in either a modal or non-modal way. It's easily a natural choice for any time we need a dialog to fetch input from a user.

What makes it somewhat difficult for some programmers to use is that it's an asynchronous function. It opens the dialog then keeps on executing code. Here's why this can be confusing.

Let's say for example we're building a web-based instant messaging client. A feature of this client would be the ability for the user to change their nickname, even after they've already logged in (similar to how MSN works). We've decided to implement this functionality by adding a button on the IM toolbar "Change Nickname". When the user clicks it, the dialog should popup, ask them for their new nickname, and then perform whatever action should be taken to update the nickname.

The Natural-But-Doesn't-Work Method
The natural way (using synchronous code) to achieve this would be something like the following:

(Javascript code is included above. I'm not sure if this renders via RSS)

But as you can see from the code, this won't work. Why? Dialog is an asynchronous method. It's easy to get comfortable with synchronous dialogs, after all, Javascript has a built in one: input(); However in this case we need to develop a new strategy for working with an asynchronous dialog.

Now lest you be tempted to do something like while(!closed) { // do nothing }; let me just ask you now: stop!. That's fighting the system. That will most likely lock up the browser, and it's an absolute waste of resources.

Using Callbacks to Trigger Behavior
Stepping back from the problem, what we really want is that we have some code, and we want it to run only when the user has finished using the dialog boxes. Being asynchronous, the dialog function actually provides a measure for doing so: Callbacks. When we defined the OK and Cancel buttons, we specified an anonymous function() that would be called whenever that button was clicked. This is a call back. We can use this exact same mechanic to signal our other code when it should run.

But how? Here's some updated code...

(Javascript code is included above. I'm not sure if this renders via RSS)

To accomplish this would could create a "gating" function -- a function which be responsible for dispatching either a call to our dialog, or a call to the code which should be run when we have a value. This is implemented in the example code as getAndSetNewNickname. In addition to the new method we've also added a parameter to the getNewNick method: callback. This parameter will allow us to pass a function that will be called whenever the "Ok" button is clicked. In our implementation, this function happens to be the same function that's calling it. The situation we end up with is a recursive callback which will call our function a 2nd time when it has a value.

The end result? Our setNewNickname method only gets called when we actually have a value. You've successfully gotten user input, and didn't have to restore to timeouts or a weird while loop solution.

Happy jQuery'ing!

Thursday, October 1, 2009

Hacking wmd-editor into using a custom image gallery

Oh man, I feel accomplished. I just finished hacking wmd editor to use a custom image gallery being generated server-side.

If you don't know, WMD editor is a WYSIWYG editor written in Javascript.
Photobucket

By default, when you click on the 'Insert Image' button you get a prompt allowing you to enter a URL.

Insert Image Screen
Photobucket

The functionality I was looking for was instead of showing you a text box where you could paste in the URL, I wanted to see more of a gallery type interface, that showed media that both you and others had uploaded. I also wanted to have a system where multiple files could be uploaded at once, and multiple files could be inserted at once.

There were primarily two challenges in attempting to accomplish that plan:

  • The WMD editor code doesn't really come with documentation. There are a handful of in-line comments and that's all you get. For the most part, I ignored them and read the code instead.
  • The WMD editor code is a little strange in areas.
Modifying WMD Editor
For the most part, I ended up not needing to change really all that much code. WMD editor is odd, yes, but workable.

The first task was to replace the default prompt function, with my own....
command.doLinkOrImage = function(chunk, postProcessing, isImage){

// ...

if (isImage) {
// OLD: util.prompt(imageDialogText, imageDefaultText, makeLinkMarkdown);
// WMD_IMAGE_GALLERY_URL loaded from a global settings elsewhere
util.imageGallery(WMD_IMAGE_GALLERY_URL, makeLinkMarkdown);
}
else {
util.prompt(linkDialogText, linkDefaultText, makeLinkMarkdown);
}
}

This was an easy change, and allowed to to write a pretty much drop-in replacement for util.promt().

This particular bit of code is jQuery-specific. It uses a div already existing on the page, #wmd-media-gallery, and applies jQuery UI's $.dialog() method to it, converted it into a popup modal dialog. The contents of the dialog is then loaded by AJAX. The URL that is loads is being specified in a global variable that is passed to it (WMD_IMAGE_GALLERY_URL).

util.imageGallery = function(gallery_url, makeLinkFunction){
gallery = $('#wmd-media-gallery');

if (gallery.html()) {
gallery.dialog('open');
}
else {
gallery.css('visibility', 'visible');
gallery.load(gallery_url, function(){
gallery.dialog({
title: 'Media Gallery',
bgiframe: true,
height: 550,
width: 900,
modal: true,
buttons: {
'Insert': function(){
// Enumerate images; insert those checked.
$('#wmd-media-gallery .media-insert').each(function(){
if ($(this).is(':checked')) {
img_path = $(this).attr('rel');
console.log(img_path);
makeLinkFunction(img_path);
}
});
$(this).dialog('close');
},
'Cancel': function() {$(this).dialog('close'); }
}
});
});
}
};

Pretty simple so far, right?

Well, here's the tricky part. If you notice what happens when the 'Insert' button is clicked, the code will scan the HTML of the dialog, and look for any input:checked element that has class "media-insert". For those elements which are found, it searches for ones which are checked. If a checked item is found, it will grab the Image URL out of the rel="" attribute, and pass that back to WMD editor for insertion into the edit box.

Ah, but not so fast. The problem comes in that WMD editor was not originally written to insert multiple images in such a way. With the stock code, what happens is it will inset one image inside another, thus only rendering 1 image properly, and leaving a mish-mash of code around it.

It should render this:
![alt text][1]
![alt text][2]

[1]: /some/url.jpg
[2]: /some/image.jpg
What it was rendering was this:
![alt text![alt text][1]][1]
The particular code in question lies int he command.addLinkDef() function. What needs to happen is that before that function processes the 'chunk', it must first clear the settings from the previous image out of it.

var getLink = function(wholeMatch, link, id, end){
// ...
};

// This goes after the getLink function

if (chunk.after.substring(0,2) == "][") {
chunk.before += chunk.selection + chunk.after.substring(0,2);
chunk.after = chunk.after.substring(2);
var linkEnd = chunk.after.indexOf(']');
linkEnd++;

chunk.before += chunk.after.substring(0, linkEnd);
chunk.selection = "";
chunk.after = chunk.after.substring(linkEnd + 1);
}

// ...and before the chunk.before [...] code

chunk.before = chunk.before.replace(regex, getLink);

And there you have it, you can now run custom image galleries from WMD editor. I'll leave it up to your imagination as to what you can do with that, but here's what mine ended up looking like:

Editor Window

Photobucket


Inserted Image
Photobucket

If you noticed, the django-uploadify app is being used in the gallery (hence why I began working on it).

Wednesday, September 30, 2009

django-uploadify Released

So I finally joined the rest of the Django community in releasing a re-usable app on GitHub. Announcing... django-uploadify!

Uploadify is a great flash-based jQuery plugin which allows users to upload an un-fixed quantity of files through a single interface. It looks and works great.

Django-uploadify is a django app that serves as a wrapper for the functionality, allowing it to interface with Django. The app works by firing a signal whenever a file is received from Uploadify, allowing developers to tie uploadify into their existing apps rather easily.

  © Blogger template 'Minimalist G' by Ourblogtemplates.com 2008

Back to TOP