<?php
/**
 * This file implements the Table class.
 *
 * This file is part of the evoCore framework - {@link http://evocore.net/}
 * See also {@link https://github.com/b2evolution/b2evolution}.
 *
 * @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
 *
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 * Parts of this file are copyright (c)2004-2006 by Daniel HAHLER - {@link http://thequod.de/contact}.
 *
 * @package evocore
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );

load_class( '_core/ui/_uiwidget.class.php', 'Widget' );

/**
 * Class Table
 *
 * @package evocore
 */
class Table extends Widget
{
	/**
	 * Total number of pages
	 */
	var $total_pages = 1;

	/**
	 * Number of cols.
	 */
	var $nb_cols;

	/**
	 * Number of lines already displayed
	 */
	var $displayed_lines_count;

	/**
	 * Number of cols already displayed (in current line)
	 */
	var $displayed_cols_count;

	/**
	 * @var array
	 */
	var $fadeout_array;

	var $fadeout_count = 0;

	/**
	 * @var boolean
	 */
	var $is_fadeout_line;

	var $no_results_text;


	/**
	 * URL param names
	 */
	var $param_prefix;


	/**
	 * Parameters for the filter area:
	 */
	var $filter_area;

	/**
	 * Filters preset button that is currently active:
	 */
	var $current_filter_preset = NULL;

	/**
	 * Parameters for the columns select area:
	 */
	var $colselect_area;

	/**
	 * Columns preset button that is currently active:
	 */
	var $current_colselect_preset = NULL;

	/**
	 * Constructor
	 *
	 * @param string template name to get from $AdminUI
	 * @param string prefix to differentiate page/order/filter params
	 */
	function __construct( $ui_template = NULL, $param_prefix = '' )
	{
		parent::__construct( $ui_template );

		// Remove unexpected chars from prefix:
		$this->param_prefix = preg_replace( '#[^a-z0-9\-_]#i', '', $param_prefix );

		$this->no_results_text = T_('No results').'.';
	}


	/**
	 * Initialize things in order to be ready for displaying.
	 *
	 * Lazy fills $this->params
	 *
	 * @param array ***please document***
	 * @param array Fadeout settings array( 'key column' => array of values ) or 'session'
	 */
	function display_init( $display_params = NULL, $fadeout = NULL )
	{
		global $AdminUI, $Session, $Debuglog;

		if( empty( $this->params ) && isset( $AdminUI ) )
		{ // Use default params from Admin Skin:
			$this->params = $AdminUI->get_template( 'Results' );
		}

		// Make sure we have display parameters:
		if( !is_null($display_params) )
		{ // Use passed params:
			//$this->params = & $display_params;
			if( !empty( $this->params ) )
			{
				$this->params = array_merge( $this->params, $display_params );
			}
			else
			{
				$this->params = & $display_params;
			}
		}

		if( empty( $this->params ) )
		{
			$this->params = array();
		}
		// Initialize default params
		$this->params = array_merge( array(
				// If button at top right: (default values for v5 skins)
				'filter_button_before' => '',
				'filter_button_after'  => '',
				'filter_button_class'  => 'filter',
				// If buttom at bottom (only happens in v7+)
				'bottom_filter_button_before' => '<div class="form-group">',
				'bottom_filter_button_after'  => '</div>',
				'bottom_filter_button_class'  => 'evo_btn_apply_filters btn-sm btn-info',
			), $this->params );

		if( $fadeout == 'session' )
		{	// Get fadeout_array from session:
			if( ($this->fadeout_array = $Session->get('fadeout_array')) && is_array( $this->fadeout_array ) )
			{
				$Debuglog->add( 'UIwidget: Got fadeout_array from session data.', 'results' );
				$Session->delete( 'fadeout_array' );
			}
			else
			{
				$this->fadeout_array = NULL;
			}
		}
		else
		{
			$this->fadeout_array = $fadeout;
		}
	}


	/**
	 * Register a filter preset
	 *
	 * @param string Preset code
	 * @param string Preset title
	 * @param string Preset URL
	 */
	function register_filter_preset( $preset_codename, $preset_label, $preset_url )
	{
		// Append a preset
		$this->filter_area['presets'][$preset_codename] = array( 
				$preset_label,
				// Append param to indicate current preset:
				url_add_param( $preset_url, $this->param_prefix.'filter_preset='.$preset_codename ) );
	}


	/**
	 * Display advanced filter fields
	 *
	 * @param object Form
	 */
	function display_advanced_filter_fields( & $Form )
	{
		if( empty( $this->filter_area['callback_advanced'] ) )
		{	// No callback function is defined for advanced filters:
			return;
		}

		// Get advanced filters from provided callback function:
		$func = $this->filter_area['callback_advanced'];
		$filter_fields = $func( $this->Form );

		if( ! is_array( $filter_fields ) )
		{	// Don't allow wrong callback for advanced filters:
			debug_die( 'Callback function <code>'.$this->filter_area['callback_advanced'].'()</code> of advanced filters must has an array in result!' );
		}

		echo '<div id="evo_results_filters"></div>';
		$Form->hidden( 'filter_query', '' );

		$js_filters = array();
		foreach( $filter_fields as $field_ID => $params )
		{
			if( $field_ID == '#default' )
			{	// Skip a reserved field for default filters:
				continue;
			}

			$js_filter = array( 'id:\''.$field_ID.'\'' );

			if( isset( $params['type'] ) )
			{	// Set default params depending on field type:
				switch( $params['type'] )
				{
					case 'date':
						$params['operators'] = '=,!=,>,>=,<,<=,between,not_between';
						$params['plugin'] = 'datepicker';
						$params['plugin_config'] = array(
							'dateFormat'  => jquery_datepicker_datefmt(),
							'monthNames'  => jquery_datepicker_month_names(),
							'dayNamesMin' => jquery_datepicker_day_names(),
							'firstDay'    => locale_startofweek(),
						);
						if( ! isset( $params['validation'] ) )
						{
							$params['validation'] = array();
						}
						$params['validation']['format'] = strtoupper( jquery_datepicker_datefmt() );
						break;
				}
			}

			if( ! isset( $params['operators'] ) )
			{	// Use default operator if it is not defined:
				$params['operators'] = '=,!=';
			}

			foreach( $params as $param_name => $param_value )
			{
				switch( $param_name )
				{
					case 'operators':
						// Convert operators to proper format:
						if( ! empty( $param_value ) )
						{
							$operators = explode( ',', $param_value );
							foreach( $operators as $o => $operator )
							{	// Replace aliases with corrent name which is used in jQuery QueryBuilder plugin:
								$operators[ $o ] = get_querybuilder_operator( $operator );
							}
							$param_value = '[\''.implode( '\',\'', $operators ).'\']';
						}
						break;

					case 'values':
						$param_values = array();
						foreach( $param_value as $sub_param_name => $sub_param_value )
						{
							$param_values[] = '{\''.format_to_js( $sub_param_name ).'\':\''.format_to_js( $sub_param_value ).'\'}';
						}
						$param_value = '['.implode( ',', $param_values ).']';
						break;

					case 'valueGetter':
					case 'valueSetter':
						// Don't convert these params to string because they are functions:
						break;

					case 'input':
						if( strpos( $param_value, 'function' ) === 0 )
						{	// Don't convert this param to string if it is a function:
							break;
						}

					default:
						if( is_array( $param_value ) )
						{	// Array param:
							$param_values = array();
							foreach( $param_value as $sub_param_name => $sub_param_value )
							{
								if( $sub_param_value == 'true' || $sub_param_value == 'false' ||
								    strpos( $sub_param_value, '[' ) === 0 )
								{	// This is a not string value:
									$sub_param_value = $sub_param_value;
								}
								else
								{	// This is a string value:
									$sub_param_value = '\''.format_to_js( $sub_param_value ).'\'';
								}
								$param_values[] = $sub_param_name.':'.$sub_param_value;
							}
							$param_value = '{'.implode( ',', $param_values ).'}';
						}
						else
						{	// String param:
							$param_value = '\''.format_to_js( $param_value ).'\'';
						}
				}

				$js_filter[] = $param_name.':'.$param_value;
			}
			$js_filters[] = '{'.implode( ',', $js_filter ).'}';
		}

		// Get filter values from request:
		$filter_query = param_condition( 'filter_query', '', false, array_keys( $filter_fields ) );
		if( empty( $filter_query ) || $filter_query === 'null' )
		{	// Set filter values if no request yet:
			$filter_query = array(
				'rules'     => array(),
				'valid'     => true,
			);
			if( isset( $filter_fields['#default'] ) )
			{	// Set filters from default config:
				foreach( $filter_fields['#default'] as $def_filter_id => $def_filter_data )
				{
					$filter_query['rules'][] = array(
						'id'       => $def_filter_id,
						'operator' => get_querybuilder_operator( is_array( $def_filter_data ) && isset( $def_filter_data[0] ) ? $def_filter_data[0] : $def_filter_data ),
						'value'    => is_array( $def_filter_data ) && isset( $def_filter_data[1] ) ? $def_filter_data[1] : '',
					);
				}
			}
			$filter_query = json_encode( $filter_query );
		}
?>
<script>
jQuery( document ).ready( function()
{
	jQuery( '#evo_results_filters' ).queryBuilder(
	{
		allow_empty: true,
		display_empty_filter: true,
		plugins: ['bt-tooltip-errors'],
		icons: {
			add_group: 'fa fa-plus-circle',
			add_rule: 'fa fa-plus',
			remove_group: 'fa fa-minus-circle',
			remove_rule: 'fa fa-minus-circle',
			error: 'fa fa-warning',
		},
		operators: [
			'equal', 'not_equal', 'less', 'less_or_equal', 'greater', 'greater_or_equal', 'between', 'not_between', 'contains', 'not_contains',
			{ type: 'blank', nb_inputs: 1, multiple: false, apply_to: ['string'] },
			{ type: 'user_tagged', nb_inputs: 1, multiple: false, apply_to: ['string'] },
			{ type: 'user_not_tagged', nb_inputs: 1, multiple: false, apply_to: ['string'] }
		],
		lang: {
			add_rule: '<?php echo TS_('Add line'); ?>',
			delete_rule: '<?php echo TS_('Remove line'); ?>',
			add_group: '<?php echo TS_('Add group'); ?>',
			delete_rule: '<?php echo TS_('Remove line'); ?>',
			delete_group: '<?php echo TS_('Remove group'); ?>',
			conditions: {
			   AND: 'Match ALL of',
    			OR: 'Match ANY of',
    		},
    		operators: {
				equal: '=',
				not_equal: '&#8800;',
				less: '<',
				less_or_equal: '&#8804;',
				greater: '>',
				greater_or_equal: '&#8805;',
				between: '<?php echo TS_('between'); ?>',
				not_between: '<?php echo TS_('not between'); ?>',
				contains: '<?php echo TS_('contains'); ?>',
				not_contains: '<?php echo TS_('doesn\'t contain'); ?>',
				blank: ' ',
				user_tagged: '<?php echo TS_('user is tagged with all of'); ?>',
				user_not_tagged: '<?php echo TS_('user is not tagged with any of'); ?>',
			}
		},
		templates: {
			group: '\
<dl id="{{= it.group_id }}" class="rules-group-container"> \
  <dt class="rules-group-header"> \
    <div class="btn-group pull-right group-actions"> \
      {{? it.level>1 }} \
        <button type="button" class="btn btn-xs btn-default" data-delete="group"> \
          <i class="{{= it.icons.remove_group }}"></i> {{= it.translate("delete_group") }} \
        </button> \
      {{?}} \
    </div> \
    <div class="btn-group group-conditions"> \
      {{~ it.conditions: condition }} \
        <label class="btn btn-xs btn-default"> \
          <input type="radio" name="{{= it.group_id }}_cond" value="{{= condition }}"> {{= it.translate("conditions", condition) }} \
        </label> \
      {{~}} \
    </div> \
    {{? it.settings.display_errors }} \
      <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
    {{?}} \
  </dt> \
  <dd class=rules-group-body> \
    <ul class=rules-list>\
    <li class="rule-container">\
      <button type="button" class="btn btn-xs btn-default" data-add="rule"> \
        <i class="{{= it.icons.add_rule }}"></i> {{= it.translate("add_rule") }} \
      </button> \
      {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \
        <button type="button" class="btn btn-xs btn-default" data-add="group"> \
          <i class="{{= it.icons.add_group }}"></i> {{= it.translate("add_group") }} \
        </button> \
      {{?}} \
    </li> \
    </ul> \
  </dd> \
</dl>',
			rule: '\
<li id="{{= it.rule_id }}" class="rule-container"> \
  <div class="rule-header"> \
    <div class="btn-group pull-right rule-actions"> \
      <button type="button" class="btn btn-xs btn-default" data-delete="rule"> \
        <i class="{{= it.icons.remove_rule }}"></i> {{= it.translate("delete_rule") }} \
      </button> \
    </div> \
  </div> \
  {{? it.settings.display_errors }} \
    <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
  {{?}} \
  <div class="rule-filter-container"></div> \
  <div class="rule-operator-container"></div> \
  <div class="rule-value-container"></div> \
</li>',
		},	
		filters: [<?php echo implode( ',', $js_filters ); ?>],
		rules: <?php echo param_format_condition( $filter_query, 'js', array_keys( $filter_fields ) ); ?>,
	} );

	// Prepare form before submitting:
	jQuery( '#evo_results_filters' ).closest( 'form' ).on( 'submit', function()
	{
		// Convert filter fields to JSON format:
		var result = jQuery( '#evo_results_filters' ).queryBuilder( 'getRules' );
		if( result === null )
		{	// Stop submitting on wrong SQL:
			return false;
		}
		else
		{	// Set query rules to hidden field before submitting:
			jQuery( 'input[name=filter_query]' ).val( JSON.stringify( result ) );
		}
	} );

	// Fix space of blank hidden operator:
	evo_fix_query_builder_blank_operator();
	jQuery( '#evo_results_filters' ).on( 'afterUpdateRuleFilter.queryBuilder.filter', function()
	{
		evo_fix_query_builder_blank_operator();
	} );
	function evo_fix_query_builder_blank_operator()
	{
		jQuery( '.rule-container .rule-operator-container' ).each( function()
		{
			if( jQuery( this ).find( 'option' ).length == jQuery( this ).find( 'option[value=blank]' ).length )
			{	// Hide container if rule uses only single blank operator:
				jQuery( this ).hide();
			}
			else
			{	// Show container with other operators:
				jQuery( this ).show();
			}
		} );
	}
} );
</script>
<?php
	}


	/**
	 * Display the column selection
	 */
	function display_colselect()
	{
		if( empty( $this->colselect_area ) )
		{	// We don't want to display a col selection section:
			return;
		}

		if( empty( $this->param_prefix ) )
		{	// Deny to use a list without prefix
			debug_die( 'You must define a $param_prefix before you can use columns select area.' );
		}

		$option_name = $this->param_prefix.'colselect';

		// "Columns" with JS toggle to reveal form:
		echo '<span onclick="toggle_filter_area(\''.$option_name.'\')"'
			.' class="btn btn-xs btn-info">'
			.get_icon( 'filters_show', 'imgtag', array( 'id' => 'clickimg_'.$option_name ) )
			.' '.T_('Columns')
		.'</span>';

		// Begining of the div with columns:
		echo '<div id="clickdiv_'.$option_name.'" style="display:none">';

		echo '</div>';
	}


	/**
	 * Display the filtering form
	 */
	function display_filters()
	{
		global $debug, $Session;

		if( empty( $this->filter_area ) )
		{	// We don't want to display a filters section:
			return;
		}

		if( empty( $this->param_prefix ) )
		{	// Deny to use a list without prefix
			debug_die( 'You must define a $param_prefix before you can use filters.' );
		}

		$custom_option_name = $this->param_prefix.'custom_filters';
		$advanced_option_name = $this->param_prefix.'advanced_filters';
		$preset_name = $this->param_prefix.'filter_preset';
		$submit_title = ( empty( $this->filter_area['submit_title'] ) ? T_('Apply filters') : $this->filter_area['submit_title'] );

		// Do we already have a form?
		$create_new_form = ! isset( $this->Form );

		echo $this->replace_vars( $this->params['filters_start'] );

		$this->current_filter_preset = param( $preset_name, 'string', NULL );
		if( $this->current_filter_preset !== NULL )
		{	// Store new preset in Session:
			$Session->set( $preset_name, $this->current_filter_preset );
			$Session->dbsave();
		}
		if( $this->current_filter_preset === NULL )
		{	// Try to get preset from Session:
			$this->current_filter_preset = $Session->get( $preset_name );
		}
		if( $this->current_filter_preset === NULL )
		{	// Use 'all' preset filter by default:
			$this->current_filter_preset = 'all';
		}

		$custom_fold_state = ( $this->current_filter_preset == 'custom' ? 'expanded' : 'collapsed' );
		$advanced_fold_state = ( $this->current_filter_preset == 'advanced' ? 'expanded' : 'collapsed' );

		//____________________________________ Filter presets ____________________________________

		echo '<span class="btn-group">';

		// Display all presers;
		if( ! empty( $this->filter_area['presets'] ) )
		{	// Display preset filters:
			
			foreach( $this->filter_area['presets'] as $key => $preset )
			{
				// Link for preset filter:
				echo '<a href="'.$preset[1].'" class="btn btn-xs btn-info'.( $this->current_filter_preset == $key ? ' active' : '' ).'">'.$preset[0].'</a>';
			}
		}

		if( ! empty( $this->filter_area['callback'] ) )
		{	// "Custom preset" with JS toggle to reveal form:
			echo '<span onclick="toggle_filter_area(\''.$custom_option_name.'\')"'
					.' class="btn btn-xs btn-info'.( $this->current_filter_preset == 'custom' ? ' active' : '' ).'">'
					.get_icon( ( $custom_fold_state == 'collapsed' ? 'filters_show' : 'filters_hide' ), 'imgtag', array( 'id' => 'clickimg_'.$custom_option_name ) )
					.' '.T_('Custom filters')
				.'</span>';
		}

		if( is_admin_page() && ! empty( $this->filter_area['callback_advanced'] ) )
		{	// "Advanced preset" with JS toggle to reveal form ONLY on back-office:
			echo '<span onclick="toggle_filter_area(\''.$advanced_option_name.'\')"'
					.' class="btn btn-xs btn-info'.( $this->current_filter_preset == 'advanced' ? ' active' : '' ).'">'
					.get_icon( ( $advanced_fold_state == 'collapsed' ? 'filters_show' : 'filters_hide' ), 'imgtag', array( 'id' => 'clickimg_'.$advanced_option_name ) )
					.' '.T_('Advanced filters')
				.'</span>';
		}

		echo '</span>'; // End of <span class="btn-group">

		//_________________________________________________________________________________________

		if( $debug > 1 )
		{
			echo ' <span class="notes">('.$custom_option_name.':'.$custom_fold_state.'; '.$advanced_option_name.':'.$custom_fold_state.')</span>';
			echo ' <span id="asyncResponse"></span>';
		}

		// ---------------- CUSTOM FILTERS ---------------- :
		if( ! empty( $this->filter_area['callback'] ) )
		{	// Display Custom Filters Form fields:

			// Begining of the Custom filters area:
			echo '<div id="clickdiv_'.$custom_option_name.'"'.( $custom_fold_state == 'collapsed' ? ' style="display:none"' : '' ).'>';

			if( $create_new_form )
			{	// We do not already have a form surrounding the whole results list:
				$ignore = ( empty( $this->filter_area['url_ignore'] ) ? $this->page_param : $this->filter_area['url_ignore'] );

				$this->Form = new Form( regenerate_url( $ignore, '', '', '&' ), $this->param_prefix.'form_search', 'get', 'blockspan' );

				$this->Form->begin_form( '' );
			}

			if( ! isset( $this->filter_area['apply_filters_button'] ) || $this->filter_area['apply_filters_button'] != 'none' )
			{	// Display a filter button only when it is not hidden by param:  (Hidden example: BackOffice > Contents > Posts)
				echo $this->params['filter_button_before'];
				$submit_name = empty( $this->filter_area['submit'] ) ? 'colselect_submit' : $this->filter_area['submit'];
				$this->Form->button_input( array(
							'tag'   => 'button',
							'name'  => $submit_name,
							'value' => get_icon( 'filter' ).' '.$submit_title,
							'class' => $this->params['filter_button_class']
					) );
				echo $this->params['filter_button_after'];
			}

			if( ! empty( $this->force_checkboxes_to_inline ) )
			{ // Set this to TRUE in order to display all checkboxes before labels
				$this->Form->force_checkboxes_to_inline = true;
			}

			// Call function to display custom filters:
			$func = $this->filter_area['callback'];
			$func( $this->Form );

			// Use reserved preset name for filtering by submitted form:
			$this->Form->hidden( $this->param_prefix.'filter_preset', 'custom' );
			if( method_exists( $this, 'save_filterset' ) )
			{	// For table that can save filterset we should set new filter on each form submit:
				$this->Form->hidden( 'filter', 'new' );
			}

			if( $create_new_form )
			{ // We do not already have a form surrounding the whole result list:
				$this->Form->end_form( '' );
				unset( $this->Form );	// forget about this temporary form
			}

			echo '</div>'; // End of the Custom filters area.
		}

		// ---------------- ADVANCED FILTERS ---------------- :
		if( is_admin_page() && ! empty($this->filter_area['callback_advanced'] ) )
		{	// Display Advanced Filters Form fields ONLY on back-office:

			// Begining of the Advanced filters area:
			echo '<div id="clickdiv_'.$advanced_option_name.'" class="evo_results_filters__advanced"'.( $advanced_fold_state == 'collapsed' ? ' style="display:none"' : '' ).'>';

			if( $create_new_form )
			{	// We do not already have a form surrounding the whole results list:
				$ignore = ( empty( $this->filter_area['url_ignore'] ) ? $this->page_param : $this->filter_area['url_ignore'] );

				$this->Form = new Form( regenerate_url( $ignore, '', '', '&' ), $this->param_prefix.'form_search', 'get', 'blockspan' );

				$this->Form->begin_form( '' );
			}

			// Print out Advanced filters which use JavaScript plugin QueryBuilder:
			$this->display_advanced_filter_fields( $this->Form );

			// Use reserved preset name for filtering by submitted form:
			$this->Form->hidden( $this->param_prefix.'filter_preset', 'advanced' );
			if( method_exists( $this, 'save_filterset' ) )
			{	// For table that can save filterset we should set new filter on each form submit:
				$this->Form->hidden( 'filter', 'new' );
			}

			if( ! isset( $this->filter_area['apply_filters_button'] ) || $this->filter_area['apply_filters_button'] != 'none' )
			{	// Display a filter button only when it is not hidden by param:  (Hidden example: BackOffice > Contents > Posts)
				echo $this->params['bottom_filter_button_before'];
				$submit_name = empty( $this->filter_area['submit'] ) ? 'colselect_submit' : $this->filter_area['submit'];
				$this->Form->button_input( array(
						'tag'   => 'button',
						'name'  => $submit_name,
						'value' => get_icon( 'filter' ).' '.$submit_title,
						'class' => $this->params['bottom_filter_button_class']
					) );
				echo $this->params['bottom_filter_button_after'];
			}

			if( $create_new_form )
			{ // We do not already have a form surrounding the whole result list:
				$this->Form->end_form( '' );
				unset( $this->Form );	// forget about this temporary form
			}

			if( isset( $this->filter_area['advanced_defaults_jsfunc'] ) )
			{	// Display a button to change default advanced filters:
				echo '<button class="btn btn-xs btn-default evo_results_filters__defaults_btn" onclick="'.$this->filter_area['advanced_defaults_jsfunc'].'"><span class="fa fa-cog pointer"></span> '.T_('Defaults').'</button>';
			}

			echo '</div>'; // End of the Advanced filters area.
		}

		echo $this->params['filters_end'];
	}


	/**
	 * Display list/table start.
	 *
	 * Typically outputs UL or TABLE tags.
	 */
	function display_list_start()
	{
		if( $this->total_pages == 0 )
		{ // There are no results! Nothing to display!
			if( isset( $this->filter_area['is_filtered'] ) && ! $this->filter_area['is_filtered'] )
			{	// If this table list is filtered and no results then we should collapse the filter area because nothing to filter:
				echo '<script type="text/javascript">toggle_filter_area( "'.$this->param_prefix.'filters", "collapse" )</script>';
			}
			echo $this->replace_vars( $this->params['no_results_start'] );
		}
		else
		{ // We have rows to display:
			if( ! empty( $this->list_mass_actions ) )
			{	// Start form for list with mass actions:
				$this->Form = new Form();
				$this->Form->begin_form();
				if( ! empty( $this->list_form_hiddens ) )
				{
					foreach( $this->list_form_hiddens as $list_form_hidden_key => $list_form_hidden_value )
					{
						if( $list_form_hidden_key == 'crumb' )
						{	// Special hidden for crumb:
							$this->Form->add_crumb( $list_form_hidden_value );
						}
						else
						{	// Normal hidden field:
							$this->Form->hidden( $list_form_hidden_key, $list_form_hidden_value );
						}
					}
				}
			}

			$list_class = empty( $this->params['list_class'] ) ? '' : ' '.$this->params['list_class'];
			$list_attrs = empty( $this->params['list_attrib'] ) ? '' : ' '.$this->params['list_attrib'];
			echo str_replace( array( ' $list_class$', ' $list_attrib$' ), array( $list_class, $list_attrs ), $this->params['list_start'] );
		}
	}


	/**
	 * Display list/table end.
	 *
	 * Typically outputs </ul> or </table>
	 */
	function display_list_end()
	{
		if( $this->total_pages == 0 )
		{ // There are no results! Nothing to display!
			echo $this->replace_vars( $this->params['no_results_end'] );
		}
		else
		{	// We have rows to display:
			echo $this->params['list_end'];

			if( ! empty( $this->list_mass_actions ) && isset( $this->Form ) )
			{	// Start form for list with mass actions:
				$this->Form->end_form();
			}
		}
	}


	/**
	 * Display list/table head.
	 *
	 * This includes list head/title and filters.
	 * EXPERIMENTAL: also dispays <tfoot>
	 */
	function display_head()
	{
		if( is_ajax_content() )
		{	// Don't display this content on AJAX request
			return;
		}

		// DISPLAY TITLE:
		if( isset($this->title) )
		{ // A title has been defined for this result set:
			echo $this->replace_vars( $this->params['head_title'] );
		}

		// DISPLAY FILTERS:
		$this->display_filters();

		// DISPLAY COL SELECTION
		$this->display_colselect();


		// Experimental:
		/*echo $this->params['tfoot_start'];
		echo $this->params['tfoot_end'];*/
	}



	/**
	 * Display column headers
	 */
	function display_col_headers()
	{
		echo $this->params['head_start'];

		if( isset( $this->cols ) )
		{

			if( !isset($this->nb_cols) )
			{	// Needed for sort strings:
				$this->nb_cols = count($this->cols);
			}


			$th_group_activated = false;

			// Loop on all columns to see if we have th_group columns:
			foreach( $this->cols as $col )
			{
				if( isset( $col['th_group'] )	)
				{	// We have a th_group column, so break:
					$th_group_activated = true;
					break;
				}
			}

			$current_th_group_colspan = 1;
			$current_th_colspan = 1;
			$current_th_group_title = NULL;
			$current_th_title = NULL;
			$header_cells = array();

			// Loop on all columns to get an array of header cells description
			// Each header cell will have a colspan and rowspan value
			// The line 0 is reserved for th_group
			// The line 1 is reserved for th
			foreach( $this->cols as $key=>$col )
			{
				//_______________________________ TH GROUP __________________________________

				if( isset( $col['th_group'] ) )
				{	// The column has a th_group
					if( is_null( $current_th_group_title ) || $col['th_group'] != $current_th_group_title )
					{	// It's the begining of a th_group colspan (line0):

						//Initialize current th_group colspan to 1 (line0):
						$current_th_group_colspan = 1;

						// Set colspan and rowspan colum for line0 to 1:
						$header_cells[0][$key]['colspan'] = 1;
						$header_cells[0][$key]['rowspan'] = 1;
					}
					else
					{	// The column is part of a th group colspan
						// Update the first th group colspan cell
						$header_cells[0][$key-$current_th_group_colspan]['colspan']++;

						// Set the colspan column to 0 to not display it
						$header_cells[0][$key]['colspan'] = 0;
						$header_cells[0][$key]['rowspan'] = 0;

						//Update current th_group colspan to 1 (line0):
						$current_th_group_colspan++;
					}

					// Update current th group title:
					$current_th_group_title = 	$col['th_group'];
				}

				//___________________________________ TH ___________________________________

				if( is_null( $current_th_title ) || $col['th'] != $current_th_title )
				{	// It's the begining of a th colspan (line1)

					//Initialize current th colspan to 1 (line1):
					$current_th_colspan = 1;

					// Update current th title:
					$current_th_title = $col['th'];

					if( $th_group_activated  && !isset( $col['th_group'] ) )
					{ // We have to lines and the column has no th_group, so it will be a "rowspan2"

						// Set the cell colspan and rowspan values for the line0:
						$header_cells[0][$key]['colspan'] = 1;
						$header_cells[0][$key]['rowspan'] = 2;

						// Set the cell colspan and rowspan values for the line1, to do not display it:
						$header_cells[1][$key]['colspan'] = 0;
						$header_cells[1][$key]['rowspan'] = 0;
					}
					else
					{	// The cell has no rowspan
						$header_cells[1][$key]['colspan'] = 1;
						$header_cells[1][$key]['rowspan'] = 1;
					}
				}
				else
				{	// The column is part of a th colspan
					if( $th_group_activated && !isset( $col['th_group'] ) )
					{	// We have to lines and the column has no th_group, the colspan is "a rowspan 2"

						// Update the first th cell colspan in line0
						$header_cells[0][$key-$current_th_colspan]['colspan']++;

						// Set the cell colspan to 0 in line0 to not display it:
						$header_cells[0][$key]['colspan'] = 0;
						$header_cells[0][$key]['rowspan'] = 0;
					}
					else
					{ // Update the first th colspan cell in line1
						$header_cells[1][$key-$current_th_colspan]['colspan']++;
					}

					// Set the cell colspan to 0 in line1 to do not display it:
					$header_cells[1][$key]['colspan'] = 0;
					$header_cells[1][$key]['rowspan'] = 0;

					$current_th_colspan++;
				}
			}

			// ________________________________________________________________________________

			if( !$th_group_activated )
			{	// We have only the "th" line to display
				$start = 1;
			}
			else
			{	// We have the "th_group" and the "th" lines to display
				$start = 0;
			}

			//__________________________________________________________________________________

			// Loop on all headers lines:
			for( $i = $start; $i <2 ; $i++ )
			{
				echo $this->params['line_start_head'];
				// Loop on all headers lines cells to display them:
				foreach( $header_cells[$i] as $key=>$cell )
				{
					if( $cell['colspan'] )
					{	// We have to dispaly cell:
						if( $i == 0 && $cell['rowspan'] != 2 )
						{	// The cell is a th_group
							$th_title = $this->cols[$key]['th_group'];
							$col_order = isset( $this->cols[$key]['order_group'] );
						}
						else
						{	// The cell is a th
							$th_title = $this->cols[$key]['th'];
							$col_order = isset( $this->cols[$key]['order'] )
							|| isset( $this->cols[$key]['order_objects_callback'] )
							|| isset( $this->cols[$key]['order_rows_callback'] );
						}


						if( isset( $this->cols[$key]['th_class'] ) )
						{	// We have a class for the th column
							$class = $this->cols[$key]['th_class'];
						}
						else
						{	// We have no class for the th column
							$class = '';
						}

						if( $key == 0 && isset($this->params['colhead_start_first']) )
						{ // Display first column start:
							$output = $this->params['colhead_start_first'];

							// Add the total column class in the grp col start first param class:
							$output = str_replace( '$class$', $class, $output );
						}
						elseif( ( $key + $cell['colspan'] ) == (count( $this->cols) ) && isset($this->params['colhead_start_last']) )
						{ // Last column can get special formatting:
							$output = $this->params['colhead_start_last'];

							// Add the total column class in the grp col start end param class:
							$output = str_replace( '$class$', $class, $output );
						}
						else
						{ // Display regular colmun start:
							$output = $this->params['colhead_start'];

							// Replace the "class_attrib" in the grp col start param by the td column class
							$output = str_replace( '$class_attrib$', 'class="'.$class.'"', $output );
						}

						// Replace column header title attribute
						if( isset( $this->cols[$key]['th_title'] ) )
						{ // Column header title is set
							$output = str_replace( '$title_attrib$', ' title="'.$this->cols[$key]['th_title'].'"', $output );
						}
						else
						{ // Column header title is not set, replace with empty string
							$output = str_replace( '$title_attrib$', '', $output );
						}

						// Set colspan and rowspan values for the cell:
						$output = preg_replace( '#(<)([^>]*)>$#', '$1$2 colspan="'.$cell['colspan'].'" rowspan="'.$cell['rowspan'].'">' , $output );

						echo $output;

						if( $col_order )
						{ // The column can be ordered:
							$col_sort_values = $this->get_col_sort_values( $key );


							// Determine CLASS SUFFIX depending on wether the current column is currently sorted or not:
							if( !empty($col_sort_values['current_order']) )
							{ // We are currently sorting on the current column:
								$class_suffix = '_current';
							}
							else
							{	// We are not sorting on the current column:
								$class_suffix = '_sort_link';
							}

							// Display title depending on sort type/mode:
							if( $this->params['sort_type'] == 'single' )
							{ // single column sort type:

								// Title with toggle:
								echo '<a href="'.$col_sort_values['order_toggle'].'"'
											.' title="'.T_('Change Order').'"'
											.' class="single'.$class_suffix.'"'
											.'>'.$th_title.'</a>';

								// Icon for ascending sort:
								echo '<a href="'.$col_sort_values['order_asc'].'"'
											.' title="'.T_('Ascending order').'"'
											.'>'.$this->params['sort_asc_'.($col_sort_values['current_order'] == 'ASC' ? 'on' : 'off')].'</a>';

								// Icon for descending sort:
								echo '<a href="'.$col_sort_values['order_desc'].'"'
											.' title="'.T_('Descending order').'"'
											.'>'.$this->params['sort_desc_'.($col_sort_values['current_order'] == 'DESC' ? 'on' : 'off')].'</a>';

							}
							else
							{ // basic sort type (toggle single column):

								if( $col_sort_values['current_order'] == 'ASC' )
								{ // the sorting is ascending and made on the current column
									$sort_icon = $this->params['basic_sort_asc'];
								}
								elseif( $col_sort_values['current_order'] == 'DESC' )
								{ // the sorting is descending and made on the current column
									$sort_icon = $this->params['basic_sort_desc'];
								}
								else
								{ // the sorting is not made on the current column
									$sort_icon = $this->params['basic_sort_off'];
								}

								// Toggle Icon + Title
								// Set link title only if the column header title was not set
								$link_title = isset( $this->cols[$key]['th_title'] ) ? '' : ' title="'.T_('Change Order').'"';
								echo '<a href="'.$col_sort_values['order_toggle'].'"'
											.$link_title
											.' class="basic'.$class_suffix.'"'
											.'>'.$sort_icon.' '.$th_title.'</a>';

							}

						}
						elseif( $th_title )
						{ // the column can't be ordered, but we still have a header defined:
							echo '<span>'.$th_title.'</span>';
						}
						// </td>
						echo $this->params['colhead_end'];
					}
				}
				// </tr>
				echo $this->params['line_end'];
			}
		} // this->cols not set

		echo $this->params['head_end'];
	}


	/**
	 *
	 */
	function display_body_start()
	{
		echo $this->params['body_start'];

		$this->displayed_lines_count = 0;

	}


	/**
	 *
	 */
	function display_body_end()
	{
		echo $this->params['body_end'];
	}


	/**
	 *
	 */
	function display_line_start( $is_last = false, $is_fadeout_line = false )
	{
		if( $this->displayed_lines_count % 2 )
		{ // Odd line:
			if( $is_last )
				$start_tag = $this->params['line_start_odd_last'];
			else
				$start_tag = $this->params['line_start_odd'];
		}
		else
		{ // Even line:
			if( $is_last )
				$start_tag = $this->params['line_start_last'];
			else
				$start_tag = $this->params['line_start'];
		}

		if( $is_fadeout_line )
		{	// Add css class for this row to highlight:
			$start_tag = update_html_tag_attribs( $start_tag, array( 'class' => 'evo_highlight' ) );
		}

		echo $start_tag;

		$this->displayed_cols_count = 0;
	}


	/**
	 *
	 */
	function display_line_end()
	{
		echo $this->params['line_end'];

		$this->displayed_lines_count ++;
	}


	/**
	 * Start a column (data).
	 *
	 * @param array Additional attributes for the <td> tag (attr_name => attr_value).
	 * @param object|NULL Current row data (The var $row is requested inside of $this->parse_col_content() )
	 */
	function display_col_start( $extra_attr = array(), $row = NULL )
	{
		// Get colum definitions for current column:
		$col = $this->cols[$this->displayed_cols_count];

		if( isset( $col['td_class'] ) )
		{	// We have a class for the total column
			$class = $col['td_class'];
		}
		else
		{	// We have no class for the total column
			$class = '';
		}

		if( ($this->displayed_cols_count == 0) && isset($this->params['col_start_first']) )
		{ // Display first column column start:
			$output = $this->params['col_start_first'];
			// Add the total column class in the col start first param class:
			$output = str_replace( '$class$', $class, $output );
		}
		elseif( ( $this->displayed_cols_count == count($this->cols)-1) && isset($this->params['col_start_last']) )
		{ // Last column can get special formatting:
			$output = $this->params['col_start_last'];
			// Add the total column class in the col start end param class:
			$output = str_replace( '$class$', $class, $output );
		}
		else
		{ // Display regular colmun start:
			$output = $this->params['col_start'];
			// Replace the "class_attrib" in the total col start param by the td column class
			$output = str_replace( '$class_attrib$', 'class="'.$class.'"', $output );
		}

		if( isset( $col['td_colspan'] ) )
		{	// Initialize colspan attribute:
			if( method_exists( $this, 'parse_col_content' ) )
			{
				$colspan = $this->parse_col_content( $col['td_colspan'] );
				$colspan = eval( "return '$colspan';" );
			}
			else
			{
				$colspan = $col['td_colspan'];
			}
			if( $colspan < 0 )
			{	// We want to substract columns from the total count:
				$colspan = $this->nb_cols + $colspan;
			}
			elseif( $colspan == 0 )
			{	// Use a count of columns:
				$colspan = $this->nb_cols;
			}
			$colspan = intval( $colspan );
			if( $colspan === 1 )
			{	// Don't create attribute "colspan" with value "1":
				$output = str_replace( '$colspan_attrib$', '', $output );
			}
			else
			{
				$output = str_replace( '$colspan_attrib$', 'colspan="'.$colspan.'"', $output );
				// Store current colspan value in order to skip next columns:
				$this->current_colspan = $colspan;
			}
		}
		else
		{	// Remove non-HTML attrib:
			$output = str_replace( '$colspan_attrib$', '', $output );
		}

		// Custom attributes:
		// Tblue> TODO: Make this more elegant (e. g.: replace "$extra_attr$" with the attributes string).
		if( $extra_attr )
		{
			if ( ! isset ($extra_attr['format_to_output']))
			{
				$output = substr( $output, 0, -1 ).get_field_attribs_as_string( $extra_attr ).'>';
			}
			else
			{
				$format_to_output = $extra_attr['format_to_output'];
				unset($extra_attr['format_to_output']);
				$output = substr( $output, 0, -1 ).get_field_attribs_as_string( $extra_attr, $format_to_output ).'>';


			}

		}
		// Check variables in column declaration:
		$output = $this->parse_class_content( $output );
		echo $output;
	}


  /**
	 *
	 */
	function display_col_end()
	{
		echo $this->params['col_end'];

		$this->displayed_cols_count ++;
	}


	/**
	 * Widget callback for template vars.
	 *
	 * This allows to replace template vars, see {@link Widget::replace_callback()}.
	 *
	 * @return string
	 */
	function replace_callback( $matches )
	{
		// echo '['.$matches[1].']';
		switch( $matches[1] )
		{
			case 'reset_filters_button':
				// Resetting the filters is the same as applying preset 'all' (should be defined for all)
				if( !isset($this->filter_area['presets']['all'] ) )
				{	// Preset "all" not defined, we don't know how to reset.
					return '';
				}
				if( empty($this->current_filter_preset) || $this->current_filter_preset == 'all' )
				{ // No filters applied:
					return '';
				}
				return '<a href="'.$this->filter_area['presets']['all'][1].'" class="btn btn-sm btn-warning">'.get_icon('reset_filters').T_('Remove filters').'</a>';

			case 'nb_cols' :
				// Number of columns in result:
				if( !isset($this->nb_cols) )
				{
					$this->nb_cols = count($this->cols);
				}
				return $this->nb_cols;

			default :
				return parent::replace_callback( $matches );
		}
	}

	/**
	 * Handle variable subtitutions for class column contents.
	 *
	 * This is one of the key functions to look at when you want to use the Results class.
	 * - #var#
	 */
	function parse_class_content( $content )
	{
		// Make variable substitution for RAWS:
		while (preg_match('!\# (\w+) \#!ix', $content, $matchesarray))
		{ // Replace all matches to the content of the current row's cell. That means that several variables can be inserted to the class.
			if (! empty($this->rows[$this->current_idx]->{$matchesarray[1]}))
			{
				$content = str_replace($matchesarray[0],$this->rows[$this->current_idx]->{$matchesarray[1]} , $content);
			}
			else
			{
				$content = str_replace($matchesarray[0], 'NULL' , $content);
			}
		}

		while (preg_match('#% (.+?) %#ix', $content, $matchesarray))
		{
			 eval('$result = '.$matchesarray[1].';');
			 $content = str_replace($matchesarray[0],$result, $content);
		}

		return $content;
	}


	/**
	 * Init results params from skin template params. It's used when Results table is filled from ajax result.
	 *
	 * @param string the template param which can have values( 'admin', 'front' )
	 * @param string the name of the skin
	 */
	function init_params_by_skin( $skin_type, $skin_name )
	{
		switch( $skin_type )
		{
			case 'admin': // admin skin type
				global $adminskins_path;
				require_once $adminskins_path.$skin_name.'/_adminUI.class.php';
				$this->params = AdminUI::get_template( 'Results' );
				break;

			case 'front': // front office skin type
				global $skins_path;
				require_once $skins_path.$skin_name.'/_skin.class.php';
				$this->params = Skin::get_template( 'Results' );
				break;

			default:
				debug_die( 'Invalid results template param!' );
		}
	}

}

?>