From 3e1f66a0e71154d7b8ab78f7a8373ea71d207b76 Mon Sep 17 00:00:00 2001 From: Doug Bell Date: Tue, 28 Oct 2008 22:27:17 +0000 Subject: [PATCH] added: DataTable asset --- docs/upgrades/packages-7.6.2/datatable.wgpkg | Bin 0 -> 1413 bytes docs/upgrades/upgrade_7.6.1-7.6.2.pl | 47 ++ lib/WebGUI/Asset/Wobject/DataTable.pm | 296 +++++++++ lib/WebGUI/Content/AjaxI18N.pm | 68 ++ lib/WebGUI/Form/DataTable.pm | 407 ++++++++++++ lib/WebGUI/i18n/English/Asset_DataTable.pm | 19 + lib/WebGUI/i18n/English/Form_DataTable.pm | 132 ++++ www/extras/yui-webgui/build/form/datatable.js | 583 ++++++++++++++++++ www/extras/yui-webgui/build/i18n/i18n.js | 86 +++ 9 files changed, 1638 insertions(+) create mode 100644 docs/upgrades/packages-7.6.2/datatable.wgpkg create mode 100644 lib/WebGUI/Asset/Wobject/DataTable.pm create mode 100644 lib/WebGUI/Content/AjaxI18N.pm create mode 100644 lib/WebGUI/Form/DataTable.pm create mode 100644 lib/WebGUI/i18n/English/Asset_DataTable.pm create mode 100644 lib/WebGUI/i18n/English/Form_DataTable.pm create mode 100644 www/extras/yui-webgui/build/form/datatable.js create mode 100644 www/extras/yui-webgui/build/i18n/i18n.js diff --git a/docs/upgrades/packages-7.6.2/datatable.wgpkg b/docs/upgrades/packages-7.6.2/datatable.wgpkg new file mode 100644 index 0000000000000000000000000000000000000000..d186b5ef0d6be0bb08757a75e4096d26b7c7b752 GIT binary patch literal 1413 zcmV;01$z1)iwFP!000001MQewZ{kK2fO+0ufp}`AlGxlFP#P)hZW^}P4P8iR(p1r4 zPhf)Wv1Y~)w%Y&RGh>?>u1U0@Miod2FqbpuHs^f!Ah-LMWm!(On#;*|$I|cl*~`k= zwr$y!QrRk%b9wkua&mdtvin!e1!ADu1osCqS%2Ph^I#mjlyT~Py`(oaqqMeX|Ls!g z;E~gCt2%=8TBV}n4&PNCq~R}BoytP|%TC40<-wb~f9)#%2fJ5&8z=np4x&)tf-*F~ zg?uCb*WbUWp@`84GeIzyqiUI4=@B0JXxb&hSIY}_L9Lf_F2hJ*uQ`=F3S~XvBQ8EZ zMU=tpsgf7QzCYUsH&7(PqPXu9KE$5h8{$cigdUwJp_ZQD(~w4-EQ_O)5VI?eS<72g z1V0Ie&N1@3Xt2ByagIg0>FT1`60ORGeOoK`I`J_6^Lotx(5J`DSHc;k;*bQ=wnGnr zA*c_I(Wb%pCG#*-i%&c6^%UXf#Pe{tXcysaMg388s?}(m%7`@@ zJ=%Z7E_^tnzV!9X{bH7;A0RT_`Pig>x+XZuEl2O;BNBa~f;x0+2ub3z3Nx= z+3Jxa(dk+D&B>s7s)Z2VnL-8zi?@Ko#X0q(A?jlRLGb;gs2FD`TQg6Y)rRn-)~IO9 zN@SR*O4PE0sz;8oC{j6|8qjX4aLv1+qz#A|3Du?Vthie%TnKDakOlVpyYJhm-#;p2 z`OYOH+oS-HxT*h_ZL7A>|0`w3-uM3>jqOYJzqbEB)5_N(z?OUb=0?7O05eDKr>~c_+s^38KO5Z)@8f}9yP==YA%IbUHgxbz zA5|}pWr4mY4|$HcEc*vz#14=bknl1*NhV<=@6u2(>hqK1!*s*R9lHzV-%NtZbtlKe zlCfPiQP?X7*9efO7!07rdt{tft^3!z0(h_pjWsNC6x4`SgFuqp%Fk#ap9#wUbaU1ES&LgqA4YmWg9El( z)!ydGqFOP!^+qCD;F%X|3Y`)-Tjk;cl4Z6v8t2{57uh#$BUukRL*VnK4;Uon&LI%t z9}?A;XhHshNTaFdUc*L}wQOY21|QN%D?FntNT|*TM+j8PBjhH;H4Cq{^+lj`)*9WO zR!G;iN1a+_IN{$ef1ggYkaeK6iFVc^(kcvs=P$LoW%yzagVy{WdP5}hl5V$Ov*>{} z{I}JoK+o}C#V#-NA3O%!KmS>b*7m>e`OkQ6vx)zPBJg!JHUY33-?XFiC#QQg9a%@U za=q;yHTM9R@}OP|sR$kdO6kf-x^a)kk&BRrMF2}22X$}O2F7?!dW z$HKNg^2ph6YF`Dh@UXL5vyDr#huG~i!(QUv T1A7nrKRoa+{2D#503rYY169g; literal 0 HcmV?d00001 diff --git a/docs/upgrades/upgrade_7.6.1-7.6.2.pl b/docs/upgrades/upgrade_7.6.1-7.6.2.pl index e51205fda..c0fa36e5a 100644 --- a/docs/upgrades/upgrade_7.6.1-7.6.2.pl +++ b/docs/upgrades/upgrade_7.6.1-7.6.2.pl @@ -37,8 +37,54 @@ upgradeToYui26($session); addUsersOnlineMacro($session); addProfileExtrasField($session); addWorkflowToDataform( $session ); +installDataTableAsset( $session ); +installAjaxI18N( $session ); finish($session); # this line required +#---------------------------------------------------------------------------- +# installDataTableAsset +# Install the asset by creating the DB table and adding it to the config file +sub installDataTableAsset { + my $session = shift; + print "\tInstalling the DataTable asset... " unless $quiet; + + $session->db->write( <<'ENDSQL' ); + CREATE TABLE DataTable ( + assetId VARCHAR(22) BINARY NOT NULL, + revisionDate BIGINT NOT NULL, + data LONGTEXT, + templateId VARCHAR(22) BINARY, + PRIMARY KEY ( assetId, revisionDate ) + ) +ENDSQL + + my $assets = $session->config->get( "assets" ); + $assets->{ "WebGUI::Asset::Wobject::DataTable" } = { category => "basic" }; + $session->config->set( "assets", $assets ); + + print "DONE!\n" unless $quiet; +} + +#---------------------------------------------------------------------------- +# installDataTableAsset +# Install the content handler by adding it to the config file +sub installAjaxI18N { + my $session = shift; + print "\tInstalling the AjaxI18N content handler... " unless $quiet; + + my $oldHandlers = $session->config->get( "contentHandlers" ); + my @newHandlers; + for my $handler ( @{ $oldHandlers } ) { + if ( $handler eq "WebGUI::Content::Operation" ) { + push @newHandlers, "WebGUI::Content::AjaxI18N"; + } + push @newHandlers, $handler; + } + $session->config->set( "contentHandlers", \@newHandlers ); + + print "DONE!\n" unless $quiet; +} + #---------------------------------------------------------------------------- sub upgradeToYui26 { my $session = shift; @@ -195,6 +241,7 @@ sub updateTemplates { my @files = readdir(DIR); closedir(DIR); my $newFolder = undef; + $session->db->write( "UPDATE asset SET parentId='infinityandbeyond' WHERE assetId='pbversion0000000000001'" ); foreach my $file (@files) { next unless ($file =~ /\.wgpkg$/); # Fix the filename to include a path diff --git a/lib/WebGUI/Asset/Wobject/DataTable.pm b/lib/WebGUI/Asset/Wobject/DataTable.pm new file mode 100644 index 000000000..8b4d68e67 --- /dev/null +++ b/lib/WebGUI/Asset/Wobject/DataTable.pm @@ -0,0 +1,296 @@ +package WebGUI::Asset::Wobject::DataTable; + +$VERSION = "1.0.0"; + +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2008 Plain Black Corporation. +#------------------------------------------------------------------- +# Please read the legal notices (docs/legal.txt) and the license +# (docs/license.txt) that came with this distribution before using +# this software. +#------------------------------------------------------------------- +# http://www.plainblack.com info@plainblack.com +#------------------------------------------------------------------- + +use strict; +use Tie::IxHash; +use WebGUI::International; +use WebGUI::Utility; +use base 'WebGUI::Asset::Wobject'; + +#------------------------------------------------------------------- + +=head2 definition ( session, definition ) + +=cut + +sub definition { + my $class = shift; + my $session = shift; + my $definition = shift; + my $i18n = WebGUI::International->new($session, 'Asset_DataTable'); + + tie my %properties, 'Tie::IxHash', ( + data => { + fieldType => 'DataTable', + defaultValue => undef, + autoGenerate => 0, + }, + templateId => { + tab => "display", + fieldType => "template", + namespace => "DataTable", + defaultValue => "3rjnBVJRO6ZSkxlFkYh_ug", + label => $i18n->get( "editForm templateId label" ), + hoverHelp => $i18n->get( "editForm templateId description" ), + }, + ); + + push @{$definition}, { + assetName => $i18n->get('assetName'), + icon => 'Asset.gif', + autoGenerateForms => 1, + tableName => 'DataTable', + className => 'WebGUI::Asset::Wobject::DataTable', + properties => \%properties, + }; + + return $class->SUPER::definition($session, $definition); +} + +#---------------------------------------------------------------------------- + +=head2 getDataJson ( ) + +Get the data as a JSON object with the following structure: + + { + columns => [ + { + key => "Column Key", + formatter => "Column Format", + }, + ... + ], + rows => [ + { + "Column Key" => "Column Value", + ... + }, + ... + ], + } + +=cut + +sub getDataJson { + my $self = shift; + + return $self->get( "data" ); +} + +#---------------------------------------------------------------------------- + +=head2 getDataTable ( ) + +Get the YUI DataTable markup and script for this DataTable. + +=cut + +# TODO Have this method get from a WebGUI::DataSource object + +sub getDataTable { + my $self = shift; + + # This method assumes the form control has been prepared + return $self->{ _datatable }->getValueAsHtml; +} + +#---------------------------------------------------------------------------- + +=head2 getDataTemplateVars ( ) + +Get the template variables for the raw data. Returns a hash reference with +"rows" and "columns" keys. + +=cut + +# TODO Have this method get from a WebGUI::DataSource object + +sub getDataTemplateVars { + my $self = shift; + + # This method assumes the form control has been prepared + my $json = $self->{ _datatable }->getValue; + my $dt = JSON->new->decode( $json ); + + # Make row data more friendly to templates + my %cols = map { $_->{ key } => $_ } @{ $dt->{ columns } }; + for my $row ( @{ $dt->{ rows } } ) { + # Create the column loop for the row + for my $col ( @{ $dt->{ columns } } ) { + push @{ $row->{ row_columns } }, { + %{ $col }, + value => $row->{ $col->{ key } }, + }; + } + } + + return $dt; +} + +#---------------------------------------------------------------------------- + +=head2 getEditForm ( ) + +Add the data table to the edit form. + +=cut + +# TODO Get the DataSource's edit form +sub getEditForm { + my $self = shift; + my $tabform = $self->SUPER::getEditForm( @_ ); + + $tabform->getTab( "data" )->raw( + WebGUI::Form::DataTable->new( $self->session, { + name => "data", + value => $self->get( "data" ), + defaultValue => undef, + showEdit => 1, + } )->toHtml + ); + + return $tabform; + } + +#---------------------------------------------------------------------------- + +=head2 getEditTabs ( ) + +Add a tab for the data table. + +=cut + +sub getEditTabs { + my $self = shift; + my $i18n = WebGUI::International->new( $self->session, "Asset_DataTable" ); + + return ( + $self->SUPER::getEditTabs, + [ "data" => $i18n->get( "tab label data" ) ], + ); +} + +#---------------------------------------------------------------------------- + +=head2 getTemplateVars ( ) + +Get the template vars for this asset. + +=cut + +sub getTemplateVars { + my $self = shift; + my $var = $self->get; + + $var->{ url } = $self->getUrl; + $var->{ dataTable } = $self->getDataTable; + $var->{ dataJson } = $self->getDataJson; + + %{ $var } = ( + %{ $var }, + %{ $self->getDataTemplateVars }, + ); + + return $var; +} + +#---------------------------------------------------------------------------- + +=head2 prepareView ( ) + +Prepare the view. Add stuff to HEAD. + +=cut + +sub prepareView { + my $self = shift; + $self->SUPER::prepareView( @_ ); + my $session = $self->session; + + # For now, prepare the form control. + # TODO Use a WebGUI::DataSource + my $dt = WebGUI::Form::DataTable->new( $session, { + name => "data", + value => $self->get( 'data' ), + defaultValue => undef, + } ); + $dt->prepare; + $self->{ _datatable } = $dt; + + # Prepare the template + my $template = WebGUI::Asset::Template->new( $session, $self->get( "templateId" ) ); + $template->prepare; + $self->{ _template } = $template; + + return; +} + +#---------------------------------------------------------------------------- + +=head2 view ( ) + +method called by the www_view method. Returns a processed template +to be displayed within the page style. + +=cut + +sub view { + my $self = shift; + my $session = $self->session; + my $var = $self->getTemplateVars; + my $dt = $self->{ _datatable }; + my $template = $self->{ _template }; + + return $self->processTemplate( $var, undef, $template ); +} + +#---------------------------------------------------------------------------- + +=head2 www_ajaxGetData ( ) + +Get the data asynchronously. + +=cut + +sub www_ajaxGetData { + my $self = shift; + + $self->session->http->setMimeType( "application/json" ); + return $self->getDataJson; +} + +#---------------------------------------------------------------------------- + +=head2 www_ajaxUpdateData ( ) + +Update the data table asynchronously. + +=cut + +sub www_ajaxUpdateData { + my $self = shift; + my $data = $self->session->form->get( "data" ); + + if ( $data && $self->canEdit ) { + $self->update( { data => $data } ); + } + + $data ||= $self->get( "data" ); + + $self->session->http->setMimeType( "application/json" ); + return $data; +} + +1; diff --git a/lib/WebGUI/Content/AjaxI18N.pm b/lib/WebGUI/Content/AjaxI18N.pm new file mode 100644 index 000000000..8a744750b --- /dev/null +++ b/lib/WebGUI/Content/AjaxI18N.pm @@ -0,0 +1,68 @@ +package WebGUI::Content::AjaxI18N; + +=head1 LEGAL + + ------------------------------------------------------------------- + WebGUI is Copyright 2001-2008 Plain Black Corporation. + ------------------------------------------------------------------- + Please read the legal notices (docs/legal.txt) and the license + (docs/license.txt) that came with this distribution before using + this software. + ------------------------------------------------------------------- + http://www.plainblack.com info@plainblack.com + ------------------------------------------------------------------- + +=cut + +use strict; +use JSON; + +=head1 NAME + +Package WebGUI::Content::AjaxI18N + +=head1 DESCRIPTION + +A content handler to get i18n data using the WebGUI.i18n JavaScript object. + +=head1 SYNOPSIS + + use WebGUI::Content::AjaxI18N + my $output = WebGUI::Content::AjaxI18N::handler($session); + +=head1 SUBROUTINES + +These subroutines are available from this package: + +=cut + +#------------------------------------------------------------------- + +=head2 handler ( session ) + +The content handler for this package. + +=cut + +sub handler { + my ( $session ) = @_; + # Only handle op=ajaxGetI18N + return undef unless ( $session->form->get( "op" ) eq "ajaxGetI18N" ); + + my $json = $session->form->get( "request" ); + my $namespaces = JSON->new->decode( $json ); + my $i18n = WebGUI::International->new( $session ); + my $response = {}; + + for my $ns ( keys %{ $namespaces } ) { + for my $key ( @{ $namespaces->{ $ns } } ) { + $response->{ $ns }->{ $key } = $i18n->get( $key, $ns ); + } + } + + $session->http->setMimeType( "application/json" ); + return JSON->new->encode( $response ); +} + +1; + diff --git a/lib/WebGUI/Form/DataTable.pm b/lib/WebGUI/Form/DataTable.pm new file mode 100644 index 000000000..ebca55347 --- /dev/null +++ b/lib/WebGUI/Form/DataTable.pm @@ -0,0 +1,407 @@ +package WebGUI::Form::DataTable; + +=head1 LEGAL + + ------------------------------------------------------------------- + WebGUI is Copyright 2001-2008 Plain Black Corporation. + ------------------------------------------------------------------- + Please read the legal notices (docs/legal.txt) and the license + (docs/license.txt) that came with this distribution before using + this software. + ------------------------------------------------------------------- + http://www.plainblack.com info@plainblack.com + ------------------------------------------------------------------- + +=cut + +use strict; +use base 'WebGUI::Form::Control'; +use WebGUI::International; + +=head1 NAME + +Package WebGUI::Form::DataTable + +=head1 DESCRIPTION + +Create an editable table. Users can add columns and rows to the table. + +=head1 SEE ALSO + +This is a subclass of WebGUI::Form::Control. + +=head1 METHODS + +The following methods are specifically available from this class. Check the superclass for additional methods. + +=cut + +#------------------------------------------------------------------- + +=head2 definition ( [ additionalTerms ] ) + +See the super class for additional details. + +=head3 additionalTerms + +The following additional parameters have been added via this sub class. + +=head4 ajaxDataUrl + +This is the URL to get the data from, AJAX-style. + +=head4 ajaxDataFunc + +This is the ?func= to get the data from. + +=head4 ajaxSaveUrl + +This is the URL to send AJAX requests to. If this exists, will send +updates to the table asyncronously. + +=head4 ajaxSaveFunc + +This is the ?func= to send AJAX requests to. + +=head4 showEdit + +If true, will enable the table for editing. This is only necessary when +displaying the table with getValueAsHtml(). + +=cut + +sub definition { + my $class = shift; + my $session = shift; + my $definition = shift || []; + + push @{$definition}, { + showEdit => { + defaultValue => 0, + }, + ajaxDataUrl => { + defaultValue => undef, + }, + ajaxDataFunc => { + defaultValue => "view", + }, + ajaxSaveUrl => { + defaultValue => undef, + }, + ajaxSaveFunc => { + defaultValue => "view", + }, + }; + + return $class->SUPER::definition($session, $definition); +} + +#------------------------------------------------------------------- + +=head2 getDataTableHtml ( ) + +Render the DataTable + +=cut + +sub getDataTableHtml { + my $self = shift; + + $self->prepare unless $self->{_prepared}; + + my $data = JSON->new->decode( $self->getOriginalValue ); + my $id = $self->get( 'id' ); + + # Get the HTML for the table + my $html = $self->getTableHtml( $data ); + + ### Prepare the columns data + my %parsers = ( + date => "YAHOO.util.DataSource.parseDate", + number => "YAHOO.util.DataSource.parseNumber", + ); + + my %editors = ( + date => "date", + textbox => "textbox", + ); + + my @columnsJson = (); + for my $column ( @{ $data->{ columns } } ) { + # Not using a datastructure and JSON.pm because of function references for "parser" + my $columnDef = '{' + . qq{"key" : "$column->{ key }", } + . qq{"abbr" : "$column->{ key }", } + . qq{"formatter" : "$column->{ formatter }", } + . qq{"resizable" : 1, } + . qq{"sortable" : 1} + ; + + # Automatically determine the parser to use + if ( $parsers{ $column->{ formatter } } ) { + $columnDef .= qq{, "parser" : $parsers{ $column->{ formatter } }}; + }; + + # If we can edit + if ( $self->get( 'showEdit' ) ) { + # Set the editor + my $editor = $editors{ $column->{ formatter } } + || $editors{ "textbox" } + ; + $columnDef .= qq{, "editor" : "$editor"}; + } + $columnDef .= '}'; + + push @columnsJson, $columnDef; + } + my $columnsJson = "[" . join( ",", @columnsJson ) . "]"; + + ### Prepare any options + my $options = { + "showEdit" => $self->get( 'showEdit' ), + "inputName" => $self->get( 'name' ), + "ajaxDataUrl" => $self->get( 'ajaxDataUrl' ), + "ajaxDataFunc" => $self->get( 'ajaxDataFunc' ), + "ajaxSaveUrl" => $self->get( 'ajaxSaveUrl' ), + "ajaxSaveFunc" => $self->get( 'ajaxSaveFunc' ), + }; + my $optionsJson = JSON->new->encode( $options ); + + # Progressively enhance the bejesus out of it + $html .= <<"ENDJS"; + +ENDJS + + return $html; +} + +#------------------------------------------------------------------- + +=head2 getDefaultValue ( ) + +Get the default value. If none exists, return at least an appropriate +data structure. + +=cut + +sub getDefaultValue { + my $self = shift; + my $value = $self->SUPER::getDefaultValue( @_ ); + + if ( !$value ) { + $value = JSON->new->encode( + { + columns => [ + { + key => "New Column", + formatter => "text", + }, + ], + rows => [ + { + "New Column" => "Value", + }, + ], + } + ); + } + + return $value; +} + +#------------------------------------------------------------------- + +=head2 getName ( session ) + +Returns the name of the form control. + +=cut + +sub getName { + my ($class, $session) = @_; + return WebGUI::International->new($session, "Form_DataTable")->get("topicName"); +} + +#------------------------------------------------------------------- + +=head2 getOriginalValue ( ) + +Get the original value, or the default value. + +=cut + +sub getOriginalValue { + my $self = shift; + my $value = $self->SUPER::getOriginalValue( @_ ); + + if ( !$value ) { + $value = $self->getDefaultValue; + } + + return $value; +} + +#------------------------------------------------------------------- + +=head2 getValue ( [ value ] ) + +Return the data for the table in a serialized JSON object with the following +structure. + + $VAR1 = { + columns => [ + { + key => column name, + formatter => "", one of "button", "checkbox", "currency", "date", "dropdown", + "email", "link", "number", "radio", "text", "textarea", "textbox" + // FUTURE ENHANCEMENTS + editor => "", one of "text", "date", "dropdown", "radio", "check" + editorOptions => [ ... ], needed for "dropdown", "radio", "check" + resizable => 1, + sortable => 1, + }, + ], + rows => [ + { + column name => value, + column name => value, + ... + }, + ] + } + +=cut + +sub getValue { + my $self = shift; + my $value = $self->SUPER::getValue(@_); + + # If passing in a data structure, encode to JSON + if ( ref $value eq "HASH" ) { + $value = JSON->new->encode( $value ); + } + + return $value; +} + +#------------------------------------------------------------------- + +=head2 getValueAsHtml ( ) + +Get the value as HTML. Render the datatable in a non-editable form. + +=cut + +sub getValueAsHtml { + my $self = shift; + return $self->getDataTableHtml; +} + +#------------------------------------------------------------------- + +=head2 getTableHtml ( [data] ) + +Get the HTML to render the table. C is the data structure with +columns and rows to render. Defaults to C. + +=cut + +sub getTableHtml { + my $self = shift; + my $data = shift; + + my $html = '
'; + + # Only insert the table if we're not getting AJAX Data + if ( !$self->get( "ajaxDataUrl" ) ) { + $html .= ''; + + for my $column ( @{ $data->{ columns } } ) { + $html .= ''; + } + + # TODO: Add table footer + $html .= ''; + + for my $row ( @{ $data->{ rows } } ) { + $html .= ''; + + for my $column ( @{ $data->{ columns } } ) { + $html .= ''; + } + + $html .= ''; + } + + $html .= '
' . $column->{ key } . '
' . $row->{ $column->{ key } } . '
'; + } + + $html .= '
'; + + # Add hidden form element to hold JSON + if ( $self->get( 'showEdit' ) ) { + $html .= ''; + } + + return $html; +} + +#------------------------------------------------------------------- + +=head2 prepare ( ) + +Load all the script and css files we need. Call this in prepareView() if needed. +Otherwise, is called automatically. + +=cut + +sub prepare { + my $self = shift; + + # Source in the scripts + my $style = $self->session->style; + my $url = $self->session->url; + $style->setLink( $url->extras( 'yui/build/datatable/assets/skins/sam/datatable.css' ), { rel => "stylesheet", type => "text/css" } ); + $style->setScript( $url->extras( 'yui/build/yahoo-dom-event/yahoo-dom-event.js' ) ); + $style->setScript( $url->extras( 'yui/build/element/element-beta.js' ) ); + $style->setScript( $url->extras( 'yui/build/dragdrop/dragdrop-min.js' ) ); + $style->setScript( $url->extras( 'yui/build/connection/connection-min.js' ) ); + $style->setScript( $url->extras( 'yui/build/json/json-min.js' ) ); + + # Prepare the editors + if ( $self->get( 'showEdit' ) ) { + $style->setLink( $url->extras( 'yui/build/button/assets/skins/sam/button.css', { rel => "stylesheet", type => "text/css" } ) ); + $style->setLink( $url->extras( 'yui/build/calendar/assets/skins/sam/calendar.css', { rel => "stylesheet", type => "text/css" } ) ); + $style->setLink( $url->extras( 'yui/build/container/assets/skins/sam/container.css' ), { rel => "stylesheet", type => "text/css" } ); + $style->setScript( $url->extras( 'yui/build/container/container-min.js' ) ); + $style->setScript( $url->extras( 'yui/build/button/button-min.js' ) ); + $style->setScript( $url->extras( 'yui/build/calendar/calendar-min.js' ) ); + } + + $style->setScript( $url->extras( 'yui-webgui/build/i18n/i18n.js' ) ); + $style->setScript( $url->extras( 'yui/build/datasource/datasource.js' ) ); + $style->setScript( $url->extras( 'yui/build/datatable/datatable.js' ) ); + $style->setScript( $url->extras( 'yui-webgui/build/form/datatable.js' ) ); + + $self->{_prepared} = 1; + return; +} + +#------------------------------------------------------------------- + +=head2 toHtml ( ) + +Render the DataTable in an editable format. + +=cut + +sub toHtml { + my $self = shift; + $self->set( 'showEdit', 1 ); + return $self->getDataTableHtml; +} + +1; + diff --git a/lib/WebGUI/i18n/English/Asset_DataTable.pm b/lib/WebGUI/i18n/English/Asset_DataTable.pm new file mode 100644 index 000000000..943ef323a --- /dev/null +++ b/lib/WebGUI/i18n/English/Asset_DataTable.pm @@ -0,0 +1,19 @@ +package WebGUI::i18n::English::Asset_DataTable; + +use strict; + +our $I18N = { + 'assetName' => { + message => q{DataTable}, + lastUpdated => 0, + }, + + 'tab label data' => { + message => q{Data Table}, + lastUpdated => 0, + context => q{Label for the tab with the editable data table}, + }, + +}; + +1; diff --git a/lib/WebGUI/i18n/English/Form_DataTable.pm b/lib/WebGUI/i18n/English/Form_DataTable.pm new file mode 100644 index 000000000..681f71351 --- /dev/null +++ b/lib/WebGUI/i18n/English/Form_DataTable.pm @@ -0,0 +1,132 @@ +package WebGUI::i18n::English::Form_DataTable; + +use strict; + +our $I18N = { + 'topicName' => { + message => q{DataTable}, + lastUpdated => 0, + }, + + 'delete rows' => { + message => q{Delete Selected Rows}, + lastUpdated => 0, + context => q{Label for button to delete the selected rows}, + }, + + 'save' => { + message => q{Save}, + lastUpdated => 0, + context => q{Label for button to Save changes}, + }, + + "add row" => { + message => q{Add Row}, + lastUpdated => 0, + context => q{Label for button to Add Row to the table}, + }, + + "help" => { + message => q{Help}, + lastUpdated => 0, + context => q{Label for button to open the help dialog}, + }, + + "edit schema" => { + message => q{Edit Schema}, + lastUpdated => 0, + context => q{Label for button to edit the table column configuration}, + }, + + "delete confirm" => { + message => q{Are you sure you want to delete these rows?}, + lastUpdated => 0, + context => q{Message for pop-up to confirm deleting rows from the table}, + }, + + "format text" => { + message => q{Text}, + lastUpdated => 0, + context => q{Format for a plain text column}, + }, + + "format email" => { + message => q{E-mail}, + lastUpdated => 0, + context => q{Format for a column for an e-mail address}, + }, + + "format link" => { + message => q{URL}, + lastUpdated => 0, + context => q{Format for a column for URLs}, + }, + + "format number" => { + message => q{Number}, + lastUpdated => 0, + context => q{Format for a column with numbers}, + }, + + "add column" => { + message => q{Add Column}, + lastUpdated => 0, + context => q{Label for button to add a column}, + }, + + "cancel" => { + message => q{Cancel}, + lastUpdated => 0, + context => q{Label for button to cancel}, + }, + + "ok" => { + message => q{Ok}, + lastUpdated => 0, + context => q{Label for button to close an information dialog}, + }, + + "save success" => { + message => q{Table saved successfully!}, + lastUpdated => 0, + context => q{Message shown when the table save succeeds}, + }, + + "save failure" => { + message => q{Save failed! Please try again.}, + lastUpdated => 0, + context => q{Message shown when the table save fails}, + }, + + "help edit cell" => { + message => q{Double-click a cell to edit the cell. Hitting tab will save the current cell and open the next.}, + lastUpdated => 0, + context => q{How to edit a cell. Shown in the help pop-up}, + }, + + "help select row" => { + message => q{Click on a row to select it. Hold shift or ctrl to select multiple rows.}, + lastUpdated => 0, + context => q{How to select a row. Shown in the help pop-up}, + }, + + "help add row" => { + message => q{Clicking Add Row will add a row to the bottom of the table.}, + lastUpdated => 0, + context => q{How to add a row. Shown in the help pop-up}, + }, + + "help default sort" => { + message => q{By default, the table will be sorted exactly as it is when it is saved.}, + lastUpdated => 0, + context => q{How to set the default sort. Shown in the help pop-up}, + }, + + "help reorder column" => { + message => q{Drag and drop columns to reorder them.}, + lastUpdated => 0, + context => q{How to reorder columns. Shown in the help pop-up}, + }, +}; + +1; diff --git a/www/extras/yui-webgui/build/form/datatable.js b/www/extras/yui-webgui/build/form/datatable.js new file mode 100644 index 000000000..e787a374a --- /dev/null +++ b/www/extras/yui-webgui/build/form/datatable.js @@ -0,0 +1,583 @@ + +// Initialize namespace +if (typeof WebGUI == "undefined") { + var WebGUI = {}; +} +if (typeof WebGUI.Form == "undefined") { + WebGUI.Form = {}; +} + + +/** + * This object contains scripts for the DataTable form control + */ + +/**************************************************************************** + * DataTable ( containerId, columns, options ) + * Initialize a WebGUI.Form.DataTable. + * containerId is the ID of the container for the table. It is assumed the data + * is already in a table inside of the container (progressive enhancement). + * columns is an array of definitions for YAHOO.widget.Column objects + * rows is an array of key/value pairs + * options is an object literal of options with the following keys + * ajaxDataUrl : The URL to get data from + * ajaxSaveUrl : The URL to save data to + * ajaxSaveFunc : The ?func= to save data to + * ajaxSaveExtras : Any extra name=value pairs necessary to save the data + * inputName : The name of the DataTable when editing the data + * showEdit : Show the edit controls and allow inline editing of values + * + */ +WebGUI.Form.DataTable += function ( containerId, columns, options ) { + this.containerId = containerId; + this.columns = columns; + this.dataSource = undefined; + this.dataTable = undefined; + this.form = undefined; + this.options = options; + this.schemaDialog = undefined; + + /************************************************************************ + * addRow ( event, data ) + * Add a row to the bottom of the table + */ + this.addRow + = function ( event, data ) { + if ( !data ) { + // Make a blank row + data = {}; + var columns = this.dataTable.getColumnSet().getDefinitions(); + for ( var i = 0; i < columns.length; i++ ) { + data[ columns[ i ].key ] = ""; + } + } + this.dataTable.addRow( data ); + }; + + /************************************************************************ + * deleteSelectedRows ( ) + * Delete the selected rows after confirming + */ + this.deleteSelectedRows + = function ( ) { + if ( confirm( this.i18n.get( "Form_DataTable", "delete confirm" ) ) ) { + var rows = this.dataTable.getSelectedRows(); + for ( var i = 0; i < rows.length; i++ ) { + this.dataTable.deleteRow( this.dataTable.getRecord( rows[i] ) ); + } + } + }; + + /************************************************************************ + * getJson ( ) + * Get the JSON data structure for the current state of the datatable + */ + this.getJson + = function () { + var data = { rows : [], columns : [] }; + + // Get the rows + var rows = this.dataTable.getRecordSet().getRecords(); + for ( var i = 0; i < rows.length; i++ ) { + data.rows[ i ] = rows[i].getData(); + } + + // Get the columns + var cols = this.dataTable.getColumnSet().getDefinitions(); + for ( var i = 0; i < cols.length; i++ ) { + data.columns[ i ] = cols[i]; + delete data.columns[ i ].editor; + delete data.columns[ i ].editorOptions; + } + + return YAHOO.lang.JSON.stringify( data ); + }; + + /************************************************************************ + * handleEditorKeyEvent ( obj ) + * Handle a keypress when the Cell Editor is open + * Enter will close the editor and move down + * Tab will close the editor and move right. + * Use the handleTableKeyEvent() to handle the moving + * Open a new cell editor on the newly focused cell + */ + this.handleEditorKeyEvent + = function ( obj ) { + // 9 = tab, 13 = enter + var e = obj.event; + if ( e.keyCode == 9 || e.keyCode == 13 ) { + var cell = this.dataTable.getCellEditor().getTdEl(); + var nextCell = this.dataTable.getNextTdEl( cell ); + this.dataTable.saveCellEditor(); + if ( nextCell ) { + this.dataTable.showCellEditor( nextCell ); + e.returnValue = false; + e.preventDefault(); + return false; + } + else { + // No next cell, make a new row and open the editor for that one + this.dataTable.addRow( {} ); + } + // BUG: If pressing Enter, editor gets hidden right away due to YUI default event + // putting e.preventDefault() and return false here makes no difference + } + }; + + /************************************************************************ + * handleEditorShowEvent ( editor ) + * Give the focus to the editor + */ + this.handleEditorShowEvent + = function ( obj ) { + obj.editor.focus(); + setTimeout( obj.editor.focus, 500 ); + }; + + /************************************************************************ + * handleRowAdd ( ) + * Open the cell editor for the newly-added row + */ + this.handleRowAdd + = function () { + var row = this.dataTable.getLastTrEl(); + this.dataTable.showCellEditor( row.firstChild ); + this.dataTable.getCellEditor().focus(); + }; + + /************************************************************************ + * handleTableKeyEvent ( obj ) + * Handle a keypress inside the DataTable + * Space will open the cell editor + */ + this.handleTableKeyEvent + = function ( obj ) { + // 9 = tab, 13 = enter, 32 = space + var e = obj.event; + if ( e.keyCode == 32 ) { + var cell = this.dataTable.getSelectedTdEls()[0]; + if ( cell ) { + this.dataTable.showCellEditor(cell); + } + } + }; + + /************************************************************************ + * hideSchemaDialog ( ) + * Hide the schema dialog without saving changed + */ + this.hideSchemaDialog + = function () { + this.schemaDialog.cancel(); + }; + + /************************************************************************ + * initDataTable + * Initialize the data table. Called automatically when the DOM is ready + */ + this.initDataTable + = function () { + var container = document.getElementById( this.containerId ); + + // If we have an ajax datasource + if ( this.options.ajaxDataUrl ) { + this.dataSource + = new YAHOO.util.DataSource( this.options.ajaxDataUrl ); + this.dataSource.responseType = YAHOO.util.DataSource.TYPE_JSON; + this.dataSource.responseSchema = { + resultsList : "rows", + fields : this.columns + }; + } + else { + // Initialize a datasource with the table + this.dataSource + = new YAHOO.util.DataSource( YAHOO.util.Dom.get( this.containerId + "-table" ) ); + this.dataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE; + this.dataSource.responseSchema = { + fields : this.columns + }; + YAHOO.util.Dom.get( this.containerId + "-table" ).style.display = "none"; + } + + var dataTableOptions = { }; + if ( this.options.showEdit ) { + dataTableOptions.draggableColumns = true; + } + if ( this.options.ajaxDataUrl ) { + dataTableOptions.initialLoad = true; + dataTableOptions.initialRequest = ""; + } + + this.dataTable = new YAHOO.widget.DataTable( + this.containerId, + this.columns, + this.dataSource, + dataTableOptions + ); + + if ( this.options.showEdit ) { + // Add the class so our editors get the right skin + YAHOO.util.Dom.addClass( document.body, "yui-skin-sam" ); + + this.dataTable.subscribe( "cellDblclickEvent", this.dataTable.onEventShowCellEditor ); + this.dataTable.subscribe( "rowClickEvent", this.dataTable.onEventSelectRow ); + this.dataTable.subscribe( "tableKeyEvent", this.handleTableKeyEvent, this, true ); + this.dataTable.subscribe( "editorKeydownEvent", this.handleEditorKeyEvent, this, true ); + this.dataTable.subscribe( "editorShowEvent", this.handleEditorShowEvent, this, true ); + this.dataTable.subscribe( "rowAddEvent", this.handleRowAdd, this, true ); + + // Add the Help button + var help = new YAHOO.widget.Button( { + id : "help", + type : "push", + label : this.i18n.get( "Form_DataTable", "help" ), + container : this.containerId, + onclick : { + fn : this.showHelp, + scope : this + } + }); + help.setStyle( "float", "left" ); + + // Add the Edit Schema button + var editSchema = new YAHOO.widget.Button( { + id : "editSchema", + type : "push", + label : this.i18n.get( "Form_DataTable", "edit schema" ), + container : this.containerId, + onclick : { + fn : this.showSchemaDialog, + scope : this + } + } ); + editSchema.setStyle( "float", "right" ); + + // Add the Add Row and Delete Row buttons + var addRow = new YAHOO.widget.Button( { + id : "addRow", + type : "push", + label : this.i18n.get( "Form_DataTable", "add row" ), + container : this.containerId, + onclick : { + fn : this.addRow, + scope : this + } + } ); + + var deleteRow = new YAHOO.widget.Button( { + id : "deleteRow", + type : "push", + label : this.i18n.get( "Form_DataTable", "delete rows" ), + container : this.containerId, + onclick : { + fn : this.deleteSelectedRows, + scope : this + } + } ); + + // This data table will be submitted async + if ( this.options.ajaxSaveUrl ) { + var save = new YAHOO.widget.Button( { + id : "saveTable", + type : "push", + label : this.i18n.get( "Form_DataTable", "save" ), + container : this.containerId, + onclick : { + fn : this.submitToAjax, + scope : this + } + } ); + } + + // This data table will be submitted with a form + if ( this.options.inputName && !this.options.ajaxSaveUrl ) { + // Find our form + var form = document.getElementById( this.containerId ); + while ( form.nodeName != "FORM" ) { + form = form.parentNode; + } + this.form = form; + + // When form is submitted, compile the JSON + YAHOO.util.Event.addListener( form, "submit", this.submitToForm, this, true ); + } + } + }; + + /************************************************************************ + * initI18N ( ) + * Initialize the I18N that we need. Then initialize the datatable. + */ + this.initI18N + = function ( ) { + this.i18n = new WebGUI.i18n( { + namespaces : { + 'Form_DataTable' : [ + "topicName", + "save", + "delete rows", + "add row", + "help", + "edit schema", + "delete confirm", + "format text", + "format email", + "format link", + "format number", + "add column", + "cancel", + "ok", + "save success", + "save failure", + "help edit cell", + "help select row", + "help add row", + "help default sort", + "help reorder column" + ] + }, + onpreload : { + fn : this.initDataTable, + obj : this, + override : true + } + } ); + }; + // Run this automatically + YAHOO.util.Event.onDOMReady( this.initI18N, undefined, this ); + + /************************************************************************ + * showHelp ( event ) + * Show the help dialog, creating it if necessary + */ + this.showHelp + = function ( e ) { + if ( !this.helpDialog ) { + var helpDialog = new YAHOO.widget.Panel( "helpWindow", { + modal : false, + draggable : true, + zIndex : 1000 + } ); + helpDialog.setHeader( "DataTable Help" ); + helpDialog.setBody( + "
    " + + "
  • " + this.i18n.get( "Form_DataTable", "help edit cell" ) + "
  • " + + "
  • " + this.i18n.get( "Form_DataTable", "help select row" ) + "
  • " + + "
  • " + this.i18n.get( "Form_DataTable", "help add row" ) + "
  • " + + "
  • " + this.i18n.get( "Form_DataTable", "help default sort" ) + "
  • " + + "
  • " + this.i18n.get( "Form_DataTable", "help reorder column" ) + "
  • " + + "
" + ); + helpDialog.render( document.body ); + this.helpDialog = helpDialog; + } + this.helpDialog.show(); + }; + + /************************************************************************ + * showSchemaDialog ( event ) + * Show the Edit Schema YUI Dialog. Markup is in WebGUI::Form::DataTable + */ + this.showSchemaDialog + = function ( e ) { + var dg = new YAHOO.widget.Dialog( "editSchemaDialog", { + modal : true, + fixedcenter : true + }); + dg.setHeader( this.i18n.get( "Form_DataTable", "edit schema" ) ); + + var body = '
'; + var cols = this.dataTable.getColumnSet().keys; + for ( var i = 0; i < cols.length; i++ ) { + body = body + + '' + + '' + + '
' + } + body += '
'; + dg.setBody( body ); + + // Function to add a column + var addColumn = function ( e ) { + // this is the dialog + var form = this.element.getElementsByTagName( "form" )[0]; + + var newIdx = 0; + while ( form.elements[ "oldKey_" + newIdx ] ) { newIdx++; } + + var oldKey = form.elements[ "oldKey_" + (newIdx - 1) ].cloneNode(true); + oldKey.name = "oldKey_" + newIdx; + oldKey.value = ""; + form.appendChild( oldKey ); + + var newKey = form.elements[ "newKey_" + (newIdx - 1) ].cloneNode(true); + newKey.name = "newKey_" + newIdx; + newKey.value = "New Column " + newIdx; + form.appendChild( newKey ); + + var format = form.elements[ "format_" + (newIdx - 1) ].cloneNode(true); + format.name = "format_" + newIdx; + format.selectedIndex = 0; + form.appendChild( format ); + + form.appendChild( document.createElement( "br" ) ); + }; + + dg.cfg.queueProperty( "buttons", [ + { text: this.i18n.get( "Form_DataTable", "add column" ), handler: addColumn }, + { text: this.i18n.get( "Form_DataTable", "cancel" ), handler: { fn: this.hideSchemaDialog, scope: this } }, + { text: this.i18n.get( "Form_DataTable", "save" ), handler: { fn: this.updateSchema, scope: this }, isDefault: true } + ] ); + dg.render( document.body ); + this.schemaDialog = dg; + }; + + /************************************************************************ + * submitToAjax ( event ) + * Save the data table to the AJAX URL + */ + this.submitToAjax + = function ( e ) { + if ( this.options.ajaxSaveUrl ) { + var callback = { + success : function ( o ) { + var dialog = new YAHOO.widget.Panel( "savedMessage", { + modal : true, + fixedcenter : true + } ); + dialog.setBody( this.i18n.get( "Form_DataTable", "save success" ) + "
" ); + new YAHOO.widget.Button( { + id : "ok", + type : "push", + label : this.i18n.get( "Form_DataTable", "ok" ), + container : dialog.body, + onclick : { + fn : function () { this.destroy() }, + scope : dialog + } + } ); + dialog.render( document.body ); + }, + failure : function ( o ) { + var dialog = new YAHOO.widget.Panel( el, { + modal : true, + fixedcenter : true + } ); + dialog.setBody( this.i18n.get( "Form_DataTable", "save failure" ) + "
" ); + new YAHOO.widget.Button( { + id : "ok", + type : "push", + label : this.i18n.get( "Form_DataTable", "ok" ), + container : dialog.body, + onclick : { + fn : function () { this.destroy() }, + scope : dialog + } + } ); + dialog.render( document.body ); + }, + scope : this + }; + + var data = this.getJson(); + var postdata = "func=" + this.options.ajaxSaveFunc + ";" + + this.options.ajaxSaveExtras + ";" + + this.options.inputName + "=" + escape( data ) + ; + + YAHOO.util.Connect.asyncRequest( + "POST", + this.options.ajaxSaveUrl, + callback, + postdata + ); + } + }; + + /************************************************************************ + * submitToForm ( event ) + * Compile the DataTable data into the right form element + */ + this.submitToForm + = function (e) { + var elem = this.form.elements[ this.options.inputName ]; + elem.value = this.getJson(); + }; + + /************************************************************************ + * updateSchema callback + */ + this.updateSchema + = function () { + var i = 0; + var data = this.schemaDialog.getData(); + while ( data[ "newKey_" + i ] ) { + var oldKey = data[ "oldKey_" + i ]; + var newKey = data[ "newKey_" + i ]; + var format = data[ "format_" + i ][0]; + var col = this.dataTable.getColumn( oldKey ); + + // Don't allow adding multiple columns with same key + if ( oldKey != newKey && this.dataTable.getColumn( newKey ) ) { + // TODO: Log an error + i++; + continue; + } + + // If the key has changed, update the row data + if ( col && col.key != newKey ) { + var rows = this.dataTable.getRecordSet().getRecords(); + for ( var i = 0; i < rows.length; i++ ) { + rows[ i ].setData( newKey, rows[ i ].getData( oldKey ) ); + rows[ i ].setData( oldKey, undefined ); + } + } + + // Change the column info + var newCol = { + key : newKey, + formatter : format, + resizeable : ( col ? col.resizeable : 1 ), + sortable : ( col ? col.sortable : 1 ) + }; + var newIndex = col ? col.getKeyIndex() : undefined; + + // Set the editor + if ( format == "date" ) { + newCol.editor = "date"; + } + else { + newCol.editor = "textbox"; + } + + this.dataTable.insertColumn( newCol, newIndex ); + if ( col ) { + // Get a new reference so we remove the right column + var delCol; + if ( oldKey == newKey ) { + delCol = this.dataTable.getColumn( oldKey )[1]; + } + else { + delCol = this.dataTable.getColumn( oldKey ); + } + this.dataTable.removeColumn( delCol ); + } + i++; + } + + this.dataTable.render(); + this.schemaDialog.cancel(); + } + +}; diff --git a/www/extras/yui-webgui/build/i18n/i18n.js b/www/extras/yui-webgui/build/i18n/i18n.js new file mode 100644 index 000000000..e09110381 --- /dev/null +++ b/www/extras/yui-webgui/build/i18n/i18n.js @@ -0,0 +1,86 @@ + +// Initialize namespace +if (typeof WebGUI == "undefined") { + var WebGUI = {}; +} + +/**************************************************************************** + * WebGUI.i18n ( { options } ) + * Initialize an i18n object. Options is an object with the following keys: + * - url : (Optional) Specify a URL to a WebGUI site. Do not include the query string + * - namespaces : (Optional) Object of arrays of namespaces to preload. Keys are namespace names, + * and array members are i18n entries to get. + * - onpreload : (Optional) Callback object for after the i18n is loaded. + * fn : The function to call + * obj : An object to pass to the function + * override : If true, the function will be called in "obj" scope + * Requires: YUI Event, YUI Connect, and YUI JSON + * Events: + * - preload : Fired when preloading is complete + */ +WebGUI.i18n += function ( opt ) { + this.url = opt.url || ""; + this.namespaces = {}; + + this.evPreload = this.createEvent( "preload" ); + if ( opt.onpreload ) { + this.subscribe( "preload", opt.onpreload.fn, opt.onpreload.obj, opt.onpreload.override ); + } + + if ( opt.namespaces ) { + this.load( opt.namespaces, true ); + } +}; + +YAHOO.lang.augmentProto( WebGUI.i18n, YAHOO.util.EventProvider ); + +/**************************************************************************** + * get( ns, key ) + * Return the string referenced by namespace and key. If the namespace and key + * have not yet been retrieved, get it from the WebGUI server. + */ +WebGUI.i18n.prototype.get += function ( ns, key ) { + if ( typeof this.namespaces[ ns ][ key ] == "undefined" ) { + this.load( ns, [ key ] ); + } + return this.namespaces[ ns ][ key ]; +}; + +/**************************************************************************** + * load( { ns : keys, ns : keys, ... }, preload ) + * Grab the requested namespace / keys from the WebGUI server. + * keys is an array of keys to get. + * If preload is defined, will fire off the preload event + */ +WebGUI.i18n.prototype.load += function ( obj, preload ) { + var requestUrl = this.url + "?op=ajaxGetI18N" + var callback = { + failure : function ( o, preload ) { + // TODO: YUI logger for this + console.log( "Could not load i18n" ); + }, + success : function ( o ) { + var responseObj = YAHOO.lang.JSON.parse( o.responseText ); + for ( var ns in responseObj ) { + for ( var key in responseObj[ ns ] ) { + if ( !this.namespaces[ ns ] ) { + this.namespaces[ ns ] = {}; + } + this.namespaces[ ns ][ key ] = responseObj[ ns ][ key ]; + } + } + if ( o.argument.preload ) { + this.fireEvent( "preload" ); + } + }, + scope : this, + argument : { preload : preload } + }; + + var postJson = 'request=' + YAHOO.lang.JSON.stringify( obj ); + + YAHOO.util.Connect.asyncRequest( "POST", requestUrl, callback, postJson ); +};