| HTML-PopupTreeSelect-Dynamic documentation | Contained in the HTML-PopupTreeSelect-Dynamic distribution. |
HTML::PopupTreeSelect::Dynamic - dynamic version of HTML::PopupTreeSelect
This module is used just like HTML::PopupTreeSelect, with the addition
of 3 new parameters - dynamic_url, dynamic_params and include_prototype. Here's
a full example:
use HTML::PopupTreeSelect::Dynamic;
# setup your tree as a hash structure. This one sets up a tree like:
#
# - Root
# - Top Category 1
# - Sub Category 1
# - Sub Category 2
# - Top Category 2
my $data = { label => "Root",
value => 0,
children => [
{ label => "Top Category 1",
value => 1,
children => [
{ label => "Sub Category 1",
value => 2
},
{ label => "Sub Category 2",
value => 3
},
],
},
{ label => "Top Category 2",
value => 4
},
]
};
# create your HTML tree select widget. This one will call a
# javascript function 'select_category(value)' when the user selects
# a category.
my $select = HTML::PopupTreeSelect::Dynamic->new(
name => 'category',
data => $data,
title => 'Select a Category',
button_label => 'Choose',
onselect => 'select_category',
dynamic_params => 'rm=get_node');
# include it in your HTML page, for example using HTML::Template:
$template->param(category_select => $select->output);
A complete, and terribly coded, example of how to use this modules is
included in the module distribution. Look for the file called
hpts_demo.cgi.
This module provides a dynamic version of HTML::PopupTreeSelect. By dynamic I mean that the tree is sent to the client in chunks as the user clicks around the tree. In HTML::PopupTreeSelect the entire tree is sent to the client when the page is loaded, introducing a long delay for large trees. With HTML::PopupTreeSelect::Dynamic trees of virtually any size can be navigated without noticable delays.
Be aware of the following issues, some or all of which may be fixed in a future version:
data parameter
to new(). In general this is considerably less problematic than
sending the entire tree to the client, but it would be nice to remove
this potential bottleneck as well. http://prototype.conio.net/
$select = HTML::PopupTreeSelect::Dynamic->new(
dynamic_url => 'http://example.com/tree_select.cgi',
...);
dynamic_url. These should be in URL format. For example, to
set "rm" to "get_node":
$select = HTML::PopupTreeSelect::Dynamic->new(
dynamic_params => 'rm=get_node',
...);
Prototype.js that comes
from HTML::Prototype. By default it is true.
false when you are already using
prototype.js in your templates via a <script> tag.
dynamic_url and dynamic_params. A CGI.pm object
containing the data from this query must be passed as a named
parameter:
$output = $select->handle_get_node(query => $query);
http://rt.cpan.org
sam@tregar.com. Please
include the version of the module and a complete test case that
demonstrates the bug.
| HTML-PopupTreeSelect-Dynamic documentation | Contained in the HTML-PopupTreeSelect-Dynamic distribution. |
package HTML::PopupTreeSelect::Dynamic; use 5.006; use strict; use warnings; our $VERSION = '1.2'; use base 'HTML::PopupTreeSelect'; use Carp qw(croak); # template source files, included at the bottom our $TEMPLATE_SRC; our $NODE_TEMPLATE_SRC; # override new() to setup defaults for dynamic_url and dynamic_params sub new { my ($pkg, %args) = @_; my $self = $pkg->SUPER::new(%args); if ($self->{dynamic_url}) { # quote literal URL $self->{dynamic_url} = qq{"$self->{dynamic_url}"}; } else { # setup default $self->{dynamic_url} = q{window.location}; } $self->{dynamic_params} ||= ""; $self->{include_prototype} = 1 unless defined $self->{include_prototype}; return $self; } # override output to drive the dynamic template sub output { my ($self, $template) = @_; $template ||= HTML::Template->new(scalarref => \$TEMPLATE_SRC, die_on_bad_params => 0, global_vars => 1, ); if( $self->{include_prototype} ) { eval { require HTML::Prototype }; croak "requires HTML::Prototype unless 'include_prototype' option is fase" if( $@ ); my $prototype = HTML::Prototype->new(); my $js = $prototype->define_javascript_functions; $template->param(prototype_js => $js); } # setup template parameters my %param = map { ($_, $self->{$_}) } qw(name height width indent_width onselect form_field form_field_form button_label button_image title include_css resizable image_path scrollbars hide_selects hide_textareas dynamic_url dynamic_params ); # get output for the widget $template->param(%param); return $template->output; } # handle sub handle_get_node { my ($self, %args) = @_; my $query = $args{query}; croak("Missing required parameter 'query'.") unless $query; my $id = $query->param('id'); my $data = $self->{data}; my $node = $data; my $template = HTML::Template->new(scalarref => \$NODE_TEMPLATE_SRC, global_vars => 1, die_on_bad_params => 0); my @node_loop; if (not defined $id) { # return the root (handle multiple roots if an array ref) if( ref $data eq 'ARRAY' ) { my $count = 0; @node_loop = map { $self->_output_node($_, $count++) } (@$data); } elsif( ref $data eq 'HASH' ) { @node_loop = ( $self->_output_node($data, "0") ); } } else { # return the children of this node my $parent; if( ref $data eq 'ARRAY' ) { $parent = $self->_find_node($data, $id); } elsif( ref $data eq 'HASH' ) { $parent = $self->_find_node($data->children, $id); } my $child_id = 0; foreach my $node (@{$parent->{children}}) { push(@node_loop, $self->_output_node($node, "$id/$child_id")); $child_id++; } } $template->param(node_loop => \@node_loop); # setup global template parameters my %param = map { ($_, $self->{$_}) } qw(name height width indent_width onselect form_field form_field_form button_label button_image title include_css resizable image_path scrollbars hide_selects hide_textareas dynamic_url dynamic_params ); $template->param(\%param); return $template->output(); } sub _find_node { my ($self, $data, $id) = @_; # if it's a single digit, then it's a leaf if( $id =~ /^\d+$/ ) { return $data->[$id]; } else { # recurse down a level my ($car, $cdr) = split('/', $id, 2); return $self->_find_node($data->[$car]->{children}, $cdr); } } sub _output_node { my ($self, $node, $id) = @_; # setup template data for a single node my %param = (label => $node->{label}, value => $node->{value}, id => $id, open => $node->{open} ? 1 : 0, inactive => $node->{inactive} ? 1 : 0); if ($node->{children} and @{$node->{children}}) { $param{has_children} = 1; } return \%param; } $TEMPLATE_SRC = <<END; <tmpl_if include_css><style type="text/css"><!-- /* style for the box around the widget */ .hpts-outer { visibility: hidden; position: absolute; top: 0px; left: 0px; border: 2px outset #333333; background-color: #ffffff; filter: progid:DXImageTransform.Microsoft.dropShadow( Color=bababa,offx=3,offy=3,positive=true); } /* style for the box that contains the tree */ .hpts-inner { <tmpl_if scrollbars> overflow: scroll; </tmpl_if> width: <tmpl_var width>px; <tmpl_if height> height: <tmpl_var height>px; </tmpl_if> } /* title bar style. The width here will define a minimum width for the widget. */ .hpts-title { padding: 2px; margin-bottom: 4px; font-size: large; color: #ffffff; background-color: #666666; width: <tmpl_var width>px; } /* style of a block of child nodes - indents them under their parent and starts them hidden */ .hpts-block { margin-left: 24px; display: none; } /* style for the button bar at the bottom of the widget */ .hpts-bbar { padding: 3px; text-align: right; margin-top: 10px; background-color: #666666; width: <tmpl_var width>px; } /* style for the buttons at the bottom of the widget */ .hpts-button { margin-left: 15px; background-color: #ffffff; color: #000000; } /* style for selected labels */ .hpts-label-selected { background: #98ccfe; } /* style for labels after being unselected */ .hpts-label-unselected { background: #ffffff; } /* style for bottom bar used for resizing */ .hpts-botbar { background-color: #666666; width: <tmpl_var width>px; font-size: 7px; padding: 3px; } --></style></tmpl_if> <script type="text/javascript"> <!-- /* record location of mouse on each click */ var hpts_mouseX; var hpts_mouseY; var hpts_offsetX; var hpts_offsetY; var hpts_locked_titlebar; /* for moving */ var hpts_locked_botbar; /* for resizing */ var hpts_curr_width = <tmpl_if width><tmpl_var width><tmpl_else>225</tmpl_if>; var hpts_curr_height = <tmpl_if height><tmpl_var height><tmpl_else>200</tmpl_if>; hpts_lock = function(evt) { evt = (evt) ? evt : event; hpts_set_locked(evt); hpts_update_mouse(evt); if (hpts_locked_titlebar) { if (evt.pageX) { hpts_offsetX = evt.pageX - ((hpts_locked_titlebar.offsetLeft) ? hpts_locked_titlebar.offsetLeft : hpts_locked_titlebar.left); hpts_offsetY = evt.pageY - ((hpts_locked_titlebar.offsetTop) ? hpts_locked_titlebar.offsetTop : hpts_locked_titlebar.top); } else if (evt.offsetX || evt.offsetY) { hpts_offsetX = evt.offsetX - ((evt.offsetX < -2) ? 0 : document.body.scrollLeft); hpts_offsetY = evt.offsetY - ((evt.offsetY < -2) ? 0 : document.body.scrollTop); } else if (evt.clientX) { hpts_offsetX = evt.clientX - ((hpts_locked_titlebar.offsetLeft) ? hpts_locked_titlebar.offsetLeft : 0); hpts_offsetY = evt.clientY - ((hpts_locked_titlebar.offsetTop) ? hpts_locked_titlebar.offsetTop : 0); } return false; } if (hpts_locked_botbar) { if (evt.pageX) { hpts_offsetX = evt.pageX; hpts_offsetY = evt.pageY; } else if (evt.clientX) { hpts_offsetX = evt.clientX; hpts_offsetY = evt.clientY; } else if (evt.offsetX || evt.offsetY) { hpts_offsetX = evt.offsetX - ((evt.offsetX < -2) ? 0 : document.body.scrollLeft); hpts_offsetY = evt.offsetY - ((evt.offsetY < -2) ? 0 : document.body.scrollTop); } return false; } return true; } hpts_update_mouse = function(evt) { if (evt.pageX) { hpts_mouseX = evt.pageX; hpts_mouseY = evt.pageY; } else { hpts_mouseX = evt.clientX + document.documentElement.scrollLeft + document.body.scrollLeft; hpts_mouseY = evt.clientY + document.documentElement.scrollTop + document.body.scrollTop; } } hpts_set_locked = function(evt) { var target = (evt.target) ? evt.target : evt.srcElement; if (target && target.className == "hpts-title") { hpts_locked_titlebar = target.parentNode; return; } else if (target && target.className == "hpts-botbar") { hpts_locked_botbar = target.parentNode; return; } hpts_locked_titlebar = null; hpts_locked_botbar = null; return; } hpts_drag = function(evt) { evt = (evt) ? evt : event; hpts_update_mouse(evt); var titleobj = document.getElementById("<tmpl_var name>-title"); var innerobj = document.getElementById("<tmpl_var name>-inner"); var bbarobj = document.getElementById("<tmpl_var name>-bbar"); var botbarobj = document.getElementById("<tmpl_var name>-botbar"); if (hpts_locked_titlebar) { hpts_locked_titlebar.style.left = (hpts_mouseX - hpts_offsetX) + "px"; hpts_locked_titlebar.style.top = (hpts_mouseY - hpts_offsetY) + "px"; evt.cancelBubble = true; return false; } if (hpts_locked_botbar) { titleobj.style.width = (hpts_curr_width + hpts_mouseX - hpts_offsetX) + "px"; innerobj.style.width = (hpts_curr_width + hpts_mouseX - hpts_offsetX) + "px"; bbarobj.style.width = (hpts_curr_width + hpts_mouseX - hpts_offsetX) + "px"; botbarobj.style.width = (hpts_curr_width + hpts_mouseX - hpts_offsetX) + "px"; innerobj.style.height = (hpts_curr_height + hpts_mouseY - hpts_offsetY) + "px"; evt.cancelBubble = true; return false; } } hpts_release = function(evt) { hpts_locked_titlebar = null; if (hpts_locked_botbar){ var widthstr = document.getElementById("<tmpl_var name>-inner").style.width; var heightstr = document.getElementById("<tmpl_var name>-inner").style.height; hpts_curr_width = parseFloat(widthstr.substr(0,widthstr.indexOf("px"))); hpts_curr_height = parseFloat(heightstr.substr(0,heightstr.indexOf("px"))); } hpts_locked_botbar = null; } document.onmousedown = hpts_lock; document.onmousemove = hpts_drag; document.onmouseup = hpts_release; var <tmpl_var name>_selected_id = -1; var <tmpl_var name>_selected_val; var <tmpl_var name>_selected_elem; /* expand or collapse a sub-tree */ <tmpl_var name>_toggle_expand = function(id) { var obj = document.getElementById("<tmpl_var name>-desc-" + id); var plus = document.getElementById("<tmpl_var name>-plus-" + id); var node = document.getElementById("<tmpl_var name>-node-" + id); if (obj.style.display != 'block') { obj.style.display = 'block'; plus.src = "<tmpl_var image_path>minus.png"; node.src = "<tmpl_var image_path>open_node.png"; new Ajax.Updater("<tmpl_var name>-desc-" + id, <tmpl_var dynamic_url>, { method: 'get', parameters: "<tmpl_if dynamic_params><tmpl_var dynamic_params>&</tmpl_if>id=" + id, evalScripts: true }); } else { obj.style.display = 'none'; obj.innerHTTML = ''; plus.src = "<tmpl_var image_path>plus.png"; node.src = "<tmpl_var image_path>closed_node.png"; } } /* select or unselect a node */ <tmpl_var name>_toggle_select = function(id, val) { if (<tmpl_var name>_selected_id != -1) { /* turn off old selected value */ var old = document.getElementById("<tmpl_var name>-line-" + <tmpl_var name>_selected_id); old.className = "hpts-label-unselected"; } if (id == <tmpl_var name>_selected_id) { /* clicked twice, turn it off and go back to nothing selected */ <tmpl_var name>_selected_id = -1; } else { /* turn on selected item */ var new_obj = document.getElementById("<tmpl_var name>-line-" + id); new_obj.className = "hpts-label-selected"; <tmpl_var name>_selected_id = id; <tmpl_var name>_selected_val = val; } } /* it's showtime! */ <tmpl_var name>_show = function() { document.getElementById("<tmpl_var name>-inner").innerHTML = ''; new Ajax.Updater('<tmpl_var name>-inner', <tmpl_var dynamic_url>, { method: 'get', parameters: "<tmpl_var dynamic_params>", evalScripts: true }); var obj = document.getElementById("<tmpl_var name>-outer"); var x = Math.floor(hpts_mouseX - (hpts_curr_width/2)); x = (x > 2 ? x : 2); var y = Math.floor(hpts_mouseY - (hpts_curr_height/5 * 4)); y = (y > 2 ? y : 2); obj.style.left = x + "px"; obj.style.top = y + "px"; obj.style.visibility = "visible"; <tmpl_if hide_selects> for(var f = 0; f < document.forms.length; f++) { for(var x = 0; x < document.forms[f].elements.length; x++) { var e = document.forms[f].elements[x]; if (e.options) { e.style.visibility = "hidden"; } } } </tmpl_if> <tmpl_if hide_textareas> for(var f = 0; f < document.forms.length; f++) { for(var x = 0; x < document.forms[f].elements.length; x++) { var e = document.forms[f].elements[x]; if (e.rows) { e.style.visibility = "hidden"; } } } </tmpl_if> } /* user clicks the ok button */ <tmpl_var name>_ok = function() { if (<tmpl_var name>_selected_id == -1) { alert("Please select an item or click Cancel to cancel selection."); return; } /* fill in a form field if they spec'd one */ <tmpl_if form_field><tmpl_if form_field_form>document.forms["<tmpl_var form_field_form>"]<tmpl_else>document.forms[0]</tmpl_if>.elements["<tmpl_var form_field>"].value = <tmpl_var name>_selected_val;</tmpl_if> /* trigger onselect */ <tmpl_if onselect><tmpl_var onselect>(<tmpl_var name>_selected_val)</tmpl_if> <tmpl_var name>_close(); } <tmpl_var name>_cancel = function() { <tmpl_var name>_close(); } <tmpl_var name>_close = function() { /* hide window */ var obj = document.getElementById("<tmpl_var name>-outer"); obj.style.visibility = "hidden"; /* clear selection */ if (<tmpl_var name>_selected_id != -1) { <tmpl_var name>_toggle_select(<tmpl_var name>_selected_id); } <tmpl_if hide_selects> for(var f = 0; f < document.forms.length; f++) { for(var x = 0; x < document.forms[f].elements.length; x++) { var e = document.forms[f].elements[x]; if (e.options) { e.style.visibility = "visible"; } } } </tmpl_if> <tmpl_if hide_textareas> for(var f = 0; f < document.forms.length; f++) { for(var x = 0; x < document.forms[f].elements.length; x++) { var e = document.forms[f].elements[x]; if (e.rows) { e.style.visibility = "visible"; } } } </tmpl_if> } //--> </script> <tmpl_var prototype_js> <div id="<tmpl_var name>-outer" class="hpts-outer"> <div class="hpts-title" id="<tmpl_var name>-title"><tmpl_var title></div> <div class="hpts-inner" id="<tmpl_var name>-inner"></div> <div class="hpts-bbar" id="<tmpl_var name>-bbar" style="white-space:nowrap"> <input class="hpts-button" type="button" value=" Ok " onclick="<tmpl_var name>_ok()"> <input class="hpts-button" type="button" value="Cancel" onclick="<tmpl_var name>_cancel()"> </div> <tmpl_if resizable> <div id="<tmpl_var name>-botbar" class="hpts-botbar"> </div></tmpl_if> </div> <input class="hpts-button" type="button" value="<tmpl_var button_label>" onmouseup="<tmpl_var name>_show()"> END $NODE_TEMPLATE_SRC = <<END; <tmpl_loop node_loop> <div style="white-space:nowrap"> <tmpl_if has_children> <img alt="" id="<tmpl_var name>-plus-<tmpl_var id>" width=16 height=16 src="<tmpl_var image_path>plus.png" onclick="<tmpl_var name>_toggle_expand('<tmpl_var id>')"><span id="<tmpl_var name>-line-<tmpl_var id>" <tmpl_unless inactive>ondblclick="<tmpl_var name>_toggle_expand('<tmpl_var id>')" onclick="<tmpl_var name>_toggle_select('<tmpl_var id>', '<tmpl_var escape=html value>')"</tmpl_unless>> <tmpl_else> <img alt="" width=16 height=16 src="<tmpl_var image_path>L.png"><span id="<tmpl_var name>-line-<tmpl_var id>" <tmpl_unless inactive>onclick="<tmpl_var name>_toggle_select('<tmpl_var id>', '<tmpl_var escape=html value>')"</tmpl_unless>> </tmpl_if> <img id="<tmpl_var name>-node-<tmpl_var id>" width=16 height=16 src="<tmpl_var image_path>closed_node.png" alt=""> <tmpl_unless inactive><a href="javascript:void(0);"></tmpl_unless><tmpl_var label><tmpl_unless inactive></a></tmpl_unless> </span> </div> <tmpl_if has_children> <div id="<tmpl_var name>-desc-<tmpl_var id>" class="hpts-block" style="white-space: nowrap; display: none"></div> </tmpl_if> <tmpl_if open><script language="javascript"><tmpl_var name>_toggle_expand('<tmpl_var id>')</script></tmpl_if> </tmpl_loop> END 1; __END__