3 * Table browser / lister class
5 * Browsers are constructed from BrowserColumns and can support sorting
6 * and other interactive behaviour. Cells may contain data which is
7 * formatted as a link, or the entire row may be linked through an onclick
12 * @author Andrew McMillan <andrew@mcmillan.net.nz>
13 * @copyright Catalyst IT Ltd, Morphoss Ltd <http://www.morphoss.com/>
14 * @license http://gnu.org/copyleft/gpl.html GNU GPL v2 or later
17 require_once("AWLUtilities.php");
20 * Ensure that this is not set elsewhere.
22 $BrowserCurrentRow = (object) array();
27 * BrowserColumns are the basic building blocks. You can specify just the
28 * field name, and the column header or you can get fancy and specify an
29 * alignment, format string, SQL formula and cell CSS class.
46 * BrowserColumn constructor. Only the first parameter is mandatory.
48 * @param string field The name of the column in the SQL result.
49 * @param string header The text to appear in the column header on output
50 * (@see BrowserColumn::RenderHeader()). If this is not supplied then
51 * a default of the field name will be used.
52 * @param string align left|center|right - text alignment. Defaults to 'left'.
53 * @param string format A format (a-la-printf) to render data values within.
54 * (@see BrowserColumn::RenderValue()). If this is not supplied
55 * then the default will ensure the column value is displayed as-is.
56 * @param string sql Some SQL which will return the desired value to be presented as column 'field' of
57 * the result. If this is blank then the column is assumed to be a real data column.
58 * @param string class Additional classes to apply to the column header and column value cells.
59 * @param string datatype This will allow 'date' or 'timestamp' to preformat the field correctly before
60 * using it in replacements or display. Other types may be added in future.
61 * @param string $hook The name of a global function which will preprocess the column value
63 * The hook function should be defined as follows:
64 * function hookfunction( $column_value, $column_name, $database_row ) {
69 function BrowserColumn( $field, $header="", $align="", $format="", $sql="", $class="", $datatype="", $hook=null ) {
70 $this->Field
= $field;
72 $this->Header
= $header;
73 $this->Format
= $format;
74 $this->Class = $class;
75 $this->Align
= $align;
76 $this->Type
= $datatype;
77 $this->Translatable
= false;
84 * Retrieves a 'field' or '...SQL... AS field' definition for the target list of the SQL.
86 function GetTarget() {
87 if ( $this->Sql
== "" ) return $this->Field
;
88 return "$this->Sql AS $this->Field";
93 * Renders the column header cell for this column. This will be rendered as a <th>...</th>
94 * with class and alignment applied to it. Browser column headers are clickable, and the
95 * ordering will also display an 'up' or 'down' triangle with the column header that the SQL
96 * is sorted on at the moment.
98 * @param string order_field The name of the field currently being sorted on.
99 * @param string order_direction Whether the sort is Ascending or Descending.
100 * @param int browser_array_key Used this to help handle separate ordering of
101 * multiple browsers on the same page.
102 * @param string forced_order If true, then we don't allow order to be changed.
104 function RenderHeader( $order_field, $order_direction, $browser_array_key=0, $forced_order=false ) {
106 if ( $this->Align
== "" ) $this->Align
= "left";
107 $html = '<th class="'.$this->Align
.'" '. ($this->Class == "" ?
"" : "class=\"$this->Class\"") . '>';
111 if ( !$forced_order && $order_field == $this->Field
) {
112 if ( strtoupper( substr( $order_direction, 0, 1) ) == 'A' ) {
119 $image = "<img class=\"order\" src=\"$c->images/$image.gif\" alt=\"$image\" />";
121 if ( !isset($browser_array_key) ||
$browser_array_key == '' ) $browser_array_key = 0;
122 if ( !$forced_order ) $html .= '<a href="'.replace_uri_params( $_SERVER['REQUEST_URI'], array( "o[$browser_array_key]" => $this->Field
, "d[$browser_array_key]" => $direction ) ).'" class="order">';
123 $html .= ($this->Header
== "" ?
$this->Field
: $this->Header
);
124 if ( !$forced_order ) $html .= "$image</a>";
129 function SetTranslatable() {
130 $this->Translatable
= true;
133 function RenderValue( $value, $extraclass = "" ) {
136 if ( $this->Type
== 'date' ||
$this->Type
== 'timestamp') {
137 $value = $session->FormattedDate( $value, $this->Type
);
140 if ( $this->Hook
&& function_exists($this->Hook
) ) {
141 dbg_error_log( "Browser", ":Browser: Hook for $this->Hook on column $this->Field");
142 $value = call_user_func( $this->Hook
, $value, $this->Field
, $this->current_row
);
145 if ( $this->Translatable
) {
146 $value = translate($value);
149 $value = str_replace( "\n", "<br />", $value );
150 if ( substr(strtolower($this->Format
),0,3) == "<td" ) {
151 $html = sprintf($this->Format
,$value);
154 // These quite probably don't work. The CSS standard for multiple classes is 'class="a b c"' but is lightly
155 // implemented according to some web references. Perhaps modern browsers are better?
156 $class = $this->Align
. ($this->Class == "" ?
"" : " $this->Class") . ($extraclass == "" ?
"" : " $extraclass");
157 if ( $class != "" ) $class = ' class="'.$class.'"';
158 $html = sprintf('<td%s>',$class);
159 $html .= ($this->Format
== "" ?
$value : sprintf($this->Format
,$value,$value));
168 * Start a new Browser, add columns, set a join and Render it to create a basic
169 * list of records in a table.
170 * You can, of course, get a lot fancier with setting ordering, where clauses
171 * totalled columns and so forth.
188 var $OrderBrowserKey;
207 * The Browser class constructor
209 * @param string $title A title for the browser (optional).
211 function Browser( $title = "" ) {
213 $this->Title
= $title;
214 $this->SubTitle
= "";
215 $this->Distinct
= "";
219 $this->BeginRow
= "<tr class=\"row%d\">\n";
220 $this->CloseRow
= "</tr>\n";
221 $this->BeginRowArgs
= array('#even');
222 $this->Totals
= array();
223 $this->Columns
= array();
224 $this->HiddenColumns
= array();
225 $this->FieldNames
= array();
226 $this->DivOpen
= '<div id="browser">';
227 $this->DivClose
= '</div>';
228 $this->ForcedOrder
= false;
229 dbg_error_log( "Browser", ":Browser: New browser called $title");
233 * Add a column to the Browser.
235 * This constructs a new BrowserColumn, appending it to the array of columns
238 * Note that if the $format parameter starts with '<td>' the format will replace
239 * the column format, otherwise it will be used within '<td>...</td>' tags.
242 * @param string $field The name of the field.
243 * @param string $header A column header for the field.
244 * @param string $align An alignment for column values.
245 * @param string $format A sprintf format for displaying column values.
246 * @param string $sql An SQL fragment for calculating the value.
247 * @param string $class A CSS class to apply to the cells of this column.
248 * @param string $hook The name of a global function which will preprocess the column value
250 * The hook function should be defined as follows:
251 * function hookfunction( $column_value, $column_name, $database_row ) {
257 function AddColumn( $field, $header="", $align="", $format="", $sql="", $class="", $datatype="", $hook=null ) {
258 $this->Columns
[] = new BrowserColumn( $field, $header, $align, $format, $sql, $class, $datatype, $hook );
259 $this->FieldNames
[$field] = count($this->Columns
) - 1;
263 * Add a hidden column - one that is present in the SQL result, but for
264 * which there is no column displayed.
266 * This can be useful for including a value in (e.g.) clickable links or title
267 * attributes which is not actually displayed as a visible column.
269 * @param string $field The name of the field.
270 * @param string $sql An SQL fragment to calculate the field, if it is calculated.
272 function AddHidden( $field, $sql="" ) {
273 $this->HiddenColumns
[] = new BrowserColumn( $field, "", "", "", $sql );
274 $this->FieldNames
[$field] = count($this->Columns
) - 1;
278 * Set the Title for the browse.
280 * This can also be set in the constructor but if you create a template Browser
281 * and then clone it in a loop you may want to assign a different Title for each
284 * @param string $new_title The new title for the browser
286 function SetTitle( $new_title ) {
287 $this->Title
= $new_title;
292 * Accessor for the Title for the browse, which could set the title also.
294 * @param string $new_title The new title for the browser
295 * @return string The current title for the browser
297 function Title( $new_title = null ) {
298 if ( isset($new_title) ) $this->Title
= $new_title;
304 * Set the named columns to be translatable
306 * @param array $column_list The list of columns which are translatable
308 function SetTranslatable( $column_list ) {
309 $top = count($this->Columns
);
310 for( $i=0; $i < $top; $i++
) {
311 dbg_error_log( "Browser", "Comparing %s with column name list", $this->Columns
[$i]->Field
);
312 if ( in_array($this->Columns
[$i]->Field
,$column_list) ) $this->Columns
[$i]->SetTranslatable();
314 $top = count($this->HiddenColumns
);
315 for( $i=0; $i < $top; $i++
) {
316 dbg_error_log( "Browser", "Comparing %s with column name list", $this->HiddenColumns
[$i]->Field
);
317 if ( in_array($this->HiddenColumns
[$i]->Field
,$column_list) ) $this->HiddenColumns
[$i]->SetTranslatable();
322 * Set a Sub Title for the browse.
324 * @param string $sub_title The sub title string
326 function SetSubTitle( $sub_title ) {
327 $this->SubTitle
= $sub_title;
331 * Set a div for wrapping the browse.
333 * @param string $open_div The HTML to open the div
334 * @param string $close_div The HTML to open the div
336 function SetDiv( $open_div, $close_div ) {
337 $this->DivOpen
= $open_div;
338 $this->DivClose
= $close_div;
342 * Set the tables and joins for the SQL.
344 * For a single table this should just contain the name of that table, but for
345 * multiple tables it should be the full content of the SQL 'FROM ...' clause
346 * (excluding the actual 'FROM' keyword).
348 * @param string $join_list
350 function SetJoins( $join_list ) {
351 $this->Joins
= $join_list;
355 * Set a Union SQL statement.
357 * In rare cases this might be useful. It's currently a fairly simple hack
358 * which requires you to put an entire valid (& matching) UNION subclause
359 * (although without the UNION keyword).
361 * @param string $union_select
363 function SetUnion( $union_select ) {
364 $this->Union
= $union_select;
368 * Set the SQL Where clause to a specific value.
370 * The WHERE keyword should not be included.
372 * @param string $where_clause A valide SQL WHERE ... clause.
374 function SetWhere( $where_clause ) {
375 $this->Where
= $where_clause;
379 * Set the SQL DISTINCT clause to a specific value.
381 * The whole clause (except the keyword) needs to be supplied
383 * @param string $distinct The whole clause, after 'DISTINCT'
385 function SetDistinct( $distinct ) {
386 $this->Distinct
= "DISTINCT ".$distinct;
390 * Set the SQL LIMIT clause to a specific value.
392 * Only the limit number should be supplied.
394 * @param int $limit_n A number of rows to limit the SQL selection to
396 function SetLimit( $limit_n ) {
397 $this->Limit
= "LIMIT ".intval($limit_n);
401 * Set the SQL OFFSET clause to a specific value.
403 * Only the offset number
405 * @param int $offset_n A number of rows to offset the SQL selection to, based from the start of the results.
407 function SetOffset( $offset_n ) {
408 $this->Offset
= "OFFSET ".intval($offset_n);
412 * Add an [operator] ... to the SQL Where clause
414 * You will generally want to call OrWhere or AndWhere rather than
415 * this function, but hey: who am I to tell you how to code!
417 * @param string $operator The operator to combine with previous where clause parts.
418 * @param string $more_where The extra part of the where clause
420 function MoreWhere( $operator, $more_where ) {
421 if ( $this->Where
== "" ) {
422 $this->Where
= $more_where;
425 $this->Where
= "$this->Where $operator $more_where";
429 * Add an OR ... to the SQL Where clause
431 * @param string $more_where The extra part of the where clause
433 function AndWhere( $more_where ) {
434 $this->MoreWhere("AND",$more_where);
438 * Add an OR ... to the SQL Where clause
440 * @param string $more_where The extra part of the where clause
442 function OrWhere( $more_where ) {
443 $this->MoreWhere("OR",$more_where);
446 function AddGrouping( $field, $browser_array_key=0 ) {
447 if ( $this->Grouping
== "" )
448 $this->Grouping
= "GROUP BY ";
450 $this->Grouping
.= ", ";
452 $this->Grouping
.= clean_string($field);
457 * Add an ordering to the browser widget.
459 * The ordering can be overridden by GET parameters which will be
460 * rendered into the column headers so that a user can click on
461 * the column headers to control the actual order.
463 * @param string $field The name of the field to be ordered by.
464 * @param string $direction A for Ascending, otherwise it will be descending order.
465 * @param string $browser_array_key Use this to distinguish between multiple
466 * browser widgets on the same page. Leave it empty if you only
467 * have a single browser instance.
468 * @param string $secondary Use this to indicate a default secondary order
469 * which shouldn't interfere with the default primary order.
471 function AddOrder( $field, $direction, $browser_array_key=0, $secondary=0 ) {
472 $field = check_by_regex($field,'/^[^\'"!\\\\()\[\]|*\/{}&%@~;:?<>]+$/');
473 if ( ! isset($this->FieldNames
[$field]) ) return;
475 if ( !isset($this->Order
) ||
$this->Order
== "" )
476 $this->Order
= "ORDER BY ";
478 $this->Order
.= ", ";
480 if ( $secondary == 0 ) {
481 $this->OrderField
= $field;
482 $this->OrderBrowserKey
= $browser_array_key;
484 $this->Order
.= $field;
486 if ( preg_match( '/^A/i', $direction) ) {
487 $this->Order
.= " ASC";
488 if ( $secondary == 0)
489 $this->OrderDirection
= 'A';
492 $this->Order
.= " DESC";
493 if ( $secondary == 0)
494 $this->OrderDirection
= 'D';
500 * Force a particular ordering onto the browser widget.
502 * @param string $field The name of the field to be ordered by.
503 * @param string $direction A for Ascending, otherwise it will be descending order.
505 function ForceOrder( $field, $direction ) {
506 $field = clean_string($field);
507 if ( ! isset($this->FieldNames
[$field]) ) return;
509 if ( $this->Order
== "" )
510 $this->Order
= "ORDER BY ";
512 $this->Order
.= ", ";
514 $this->Order
.= $field;
516 if ( preg_match( '/^A/i', $direction) ) {
517 $this->Order
.= " ASC";
520 $this->Order
.= " DESC";
523 $this->ForcedOrder
= true;
528 * Set up the ordering for the browser. Generally you should call this with
529 * the first parameter set as a field to order by default. Call with the second
530 * parameter set to 'D' or 'DESCEND' if you want to reverse the default order.
532 function SetOrdering( $default_fld=null, $default_dir='A' , $browser_array_key=0 ) {
533 if ( isset( $_GET['o'][$browser_array_key] ) && isset($_GET['d'][$browser_array_key] ) ) {
534 $this->AddOrder( $_GET['o'][$browser_array_key], $_GET['d'][$browser_array_key], $browser_array_key );
537 if ( ! isset($default_fld) ) $default_fld = $this->Columns
[0];
538 $this->AddOrder( $default_fld, $default_dir, $browser_array_key );
544 * Mark a column as something to be totalled. You can also specify the name of
545 * a function which may modify the value before the actual totalling.
547 * The callback function will be called with each row, with the first argument
548 * being the entire record object and the second argument being only the column
549 * being totalled. The callback should return a number, to be added to the total.
551 * @param string $column_name The name of the column to be totalled.
552 * @param string $total_function The name of the callback function.
554 function AddTotal( $column_name, $total_function = false ) {
555 $this->Totals
[$column_name] = 0;
556 if ( $total_function != false ) {
557 $this->TotalFuncs
[$column_name] = $total_function;
563 * Retrieve the total from a totalled column
565 * @param string $column_name The name of the column to be totalled.
567 function GetTotal( $column_name ) {
568 return $this->Totals
[$column_name];
573 * Set the format for an output row.
575 * The row format is set as an sprintf format string for the start of the row,
576 * and a plain text string for the close of the row. Subsequent arguments
577 * are interpreted as names of fields, the values of which will be sprintf'd
578 * into the beginrow string for each row.
580 * Some special field names exist beginning with the '#' character which have
581 * 'magic' functionality, including '#even' which will insert '0' for even
582 * rows and '1' for odd rows, allowing a nice colour alternation if the
583 * beginrow format refers to it like: 'class="r%d"' so that even rows will
584 * become 'class="r0"' and odd rows will be 'class="r1"'.
586 * At present only '#even' exists, although other magic values may be defined
589 * @param string $beginrow The new printf format for the start of the row.
590 * @param string $closerow The new string for the close of the row.
591 * @param string $rowargs ... The row arguments which will be sprintf'd into
592 * the $beginrow format for each row
594 function RowFormat( $beginrow, $closerow, $rowargs )
596 $argc = func_num_args();
597 $this->BeginRow
= func_get_arg(0);
598 $this->CloseRow
= func_get_arg(1);
600 $this->BeginRowArgs
= array();
601 for( $i=2; $i < $argc; $i++
) {
602 $this->BeginRowArgs
[] = func_get_arg($i);
608 * This method is used to build and execute the database query.
610 * You need not call this method, since Browser::Render() will call it for
611 * you if you have not done so at that point.
613 * @return boolean The success / fail status of the AwlQuery::Exec()
617 foreach( $this->Columns
AS $k => $column ) {
618 if ( $target_fields != "" ) $target_fields .= ", ";
619 $target_fields .= $column->GetTarget();
621 if ( isset($this->HiddenColumns
) ) {
622 foreach( $this->HiddenColumns
AS $k => $column ) {
623 if ( $target_fields != "" ) $target_fields .= ", ";
624 $target_fields .= $column->GetTarget();
627 $where_clause = ((isset($this->Where
) && $this->Where
!= "") ?
"WHERE $this->Where" : "" );
628 $sql = sprintf( "SELECT %s %s FROM %s %s %s ", $this->Distinct
, $target_fields,
629 $this->Joins
, $where_clause, $this->Grouping
);
630 if ( "$this->Union" != "" ) {
631 $sql .= "UNION $this->Union ";
633 $sql .= $this->Order
. ' ' . $this->Limit
. ' ' . $this->Offset
;
634 $this->Query
= new AwlQuery( $sql );
635 return $this->Query
->Exec("Browse:$this->Title:DoQuery");
640 * Add an extra arbitrary row onto the end of the browser.
642 * @var array $column_values Contains an array of named fields, hopefully matching the column names.
644 function AddRow( $column_values ) {
645 if ( !isset($this->ExtraRows
) ||
typeof($this->ExtraRows
) != 'array' ) $this->ExtraRows
= array();
646 $this->ExtraRows
[] = &$column_values;
651 * Replace a row where $column = $value with an extra arbitrary row, returned from calling $function
653 * @param string $column The name of a column to match
654 * @param string $value The value to match in the column
655 * @param string $function The name of the function to call for the matched row
657 function MatchedRow( $column, $value, $function ) {
658 $this->match_column
= $column;
659 $this->match_value
= $value;
660 $this->match_function
= $function;
665 * Return values from the current row for replacing into a template.
667 * This is used to return values from the current row, so they can
668 * be inserted into a row template. It is used as a callback
669 * function for preg_replace_callback.
671 * @param array of string $matches An array containing a field name as offset 1
673 function ValueReplacement($matches)
675 // as usual: $matches[0] is the complete match
676 // $matches[1] the match for the first subpattern
677 // enclosed in '##...##' and so on
679 $field_name = $matches[1];
680 if ( !isset($this->current_row
->{$field_name}) && substr($field_name,0,4) == "URL:" ) {
681 $field_name = substr($field_name,4);
682 $replacement = urlencode($this->current_row
->{$field_name});
685 $replacement = (isset($this->current_row
->{$field_name}) ?
$this->current_row
->{$field_name} : '');
687 dbg_error_log( "Browser", ":ValueReplacement: Replacing %s with %s", $field_name, $replacement);
693 * This method is used to render the browser as HTML. If the query has
694 * not yet been executed then this will call DoQuery to do so.
696 * The browser (including the title) will be displayed in a div with id="browser" so
697 * that you can style '#browser tr.header', '#browser tr.totals' and so forth.
699 * @param string $title_tag The tag to use around the browser title (default 'h1')
700 * @return string The rendered HTML fragment to display to the user.
702 function Render( $title_tag = null, $subtitle_tag = null ) {
703 global $c, $BrowserCurrentRow;
705 if ( !isset($this->Query
) ) $this->DoQuery(); // Ensure the query gets run before we render!
707 dbg_error_log( "Browser", ":Render: browser $this->Title");
708 $html = $this->DivOpen
;
709 if ( $this->Title
!= "" ) {
710 if ( !isset($title_tag) ) $title_tag = 'h1';
711 $html .= "<$title_tag>$this->Title</$title_tag>\n";
713 if ( $this->SubTitle
!= "" ) {
714 if ( !isset($subtitle_tag) ) $subtitle_tag = 'h2';
715 $html .= "<$subtitle_tag>$this->SubTitle</$subtitle_tag>\n";
718 $html .= "<table id=\"browse_table\">\n";
719 $html .= "<thead><tr class=\"header\">\n";
720 foreach( $this->Columns
AS $k => $column ) {
721 $html .= $column->RenderHeader( $this->OrderField
, $this->OrderDirection
, $this->OrderBrowserKey
, $this->ForcedOrder
);
723 $html .= "</tr></thead>\n<tbody>";
725 $rowanswers = array();
726 while( $BrowserCurrentRow = $this->Query
->Fetch() ) {
728 // Work out the answers to any stuff that may be being substituted into the row start
729 /** @TODO: We should deprecate this approach in favour of simply doing the ValueReplacement on field names */
730 foreach( $this->BeginRowArgs
AS $k => $fld ) {
731 if ( isset($BrowserCurrentRow->{$fld}) ) {
732 $rowanswers[$k] = $BrowserCurrentRow->{$fld};
737 $rowanswers[$k] = ($this->Query
->rownum() %
2);
740 $rowanswers[$k] = $fld;
745 $row_html = vsprintf( preg_replace("/#@even@#/", ($this->Query
->rownum() %
2), $this->BeginRow
), $rowanswers);
747 if ( isset($this->match_column
) && isset($this->match_value
) && $BrowserCurrentRow->{$this->match_column
} == $this->match_value
) {
748 $row_html .= call_user_func( $this->match_function
, $BrowserCurrentRow );
752 foreach( $this->Columns
AS $k => $column ) {
753 $row_html .= $column->RenderValue( (isset($BrowserCurrentRow->{$column->Field
})?
$BrowserCurrentRow->{$column->Field
}:'') );
754 if ( isset($this->Totals
[$column->Field
]) ) {
755 if ( isset($this->TotalFuncs
[$column->Field
]) && function_exists($this->TotalFuncs
[$column->Field
]) ) {
756 // Run the amount through the callback function $floatval = my_function( $row, $fieldval );
757 $this->Totals
[$column->Field
] +
= $this->TotalFuncs
[$column->Field
]( $BrowserCurrentRow, $BrowserCurrentRow->{$column->Field
} );
760 // Just add the amount
761 $this->Totals
[$column->Field
] +
= doubleval( preg_replace( '/[^0-9.-]/', '', $BrowserCurrentRow->{$column->Field
} ));
768 $row_html .= preg_replace("/#@even@#/", ($this->Query
->rownum() %
2), $this->CloseRow
);
769 $this->current_row
= $BrowserCurrentRow;
770 $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
773 if ( count($this->Totals
) > 0 ) {
774 $BrowserCurrentRow = (object) "";
775 $row_html = "<tr class=\"totals\">\n";
776 foreach( $this->Columns
AS $k => $column ) {
777 if ( isset($this->Totals
[$column->Field
]) ) {
778 $row_html .= $column->RenderValue( $this->Totals
[$column->Field
], "totals" );
781 $row_html .= $column->RenderValue( "" );
784 $row_html .= "</tr>\n";
785 $this->current_row
= $BrowserCurrentRow;
786 $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
790 if ( count($this->ExtraRows
) > 0 ) {
791 foreach( $this->ExtraRows
AS $k => $v ) {
792 $BrowserCurrentRow = (object) $v;
793 // Work out the answers to any stuff that may be being substituted into the row start
794 foreach( $this->BeginRowArgs
AS $k => $fld ) {
795 if ( isset( $BrowserCurrentRow->{$fld} ) ) {
796 $rowanswers[$k] = $BrowserCurrentRow->{$fld};
801 $rowanswers[$k] = ($this->Query
->rownum() %
2);
804 $rowanswers[$k] = $fld;
810 $row_html = vsprintf( preg_replace("/#@even@#/", ($this->Query
->rownum() %
2), $this->BeginRow
), $rowanswers);
812 if ( isset($this->match_column
) && isset($this->match_value
) && $BrowserCurrentRow->{$this->match_column
} == $this->match_value
) {
813 $row_html .= call_user_func( $this->match_function
, $BrowserCurrentRow );
817 foreach( $this->Columns
AS $k => $column ) {
818 $row_html .= $column->RenderValue( (isset($BrowserCurrentRow->{$column->Field
}) ?
$BrowserCurrentRow->{$column->Field
} : '') );
823 $row_html .= preg_replace("/#@even@#/", ($this->Query
->rownum() %
2), $this->CloseRow
);
824 $this->current_row
= $BrowserCurrentRow;
825 $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
829 $html .= "</tbody>\n</table>\n";
830 $html .= $this->DivClose
;