<?php
/**
* This file implements Item Fields Compare Widget 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-2017 by Francois Planque - {@link http://fplanque.com/}
*
* @package evocore
*/
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
load_class( 'widgets/model/_widget.class.php', 'ComponentWidget' );
/**
* ComponentWidget Class
*
* A ComponentWidget is a displayable entity that can be placed into a Container on a web page.
*
* @package evocore
*/
class item_fields_compare_Widget extends ComponentWidget
{
var $icon = 'balance-scale';
/**
* Constructor
*/
function __construct( $db_row = NULL )
{
// Call parent constructor:
parent::__construct( $db_row, 'core', 'item_fields_compare' );
}
/**
* Get help URL
*
* @return string URL
*/
function get_help_url()
{
return get_manual_url( 'item-fields-compare-widget' );
}
/**
* Get name of widget
*/
function get_name()
{
return T_('Compare Items');
}
/**
* Get a very short desc. Used in the widget list.
*/
function get_short_desc()
{
return format_to_output( $this->disp_params['title'] );
}
/**
* Get short description
*/
function get_desc()
{
return T_('Display differences between custom fields of the selected items.');
}
/**
* Get definitions for editable params
*
* @see Plugin::GetDefaultSettings()
* @param local params like 'for_editing' => true
*/
function get_param_definitions( $params )
{
$ItemTypeCache = & get_ItemTypeCache();
$item_type_options = array(
'default' => T_('Default types shown for this collection')
) + $ItemTypeCache->get_option_array();
$r = array(
'title' => array(
'label' => T_( 'Title' ),
'size' => 40,
'note' => T_('This is the title to display'),
'defaultvalue' => '',
),
'items_source' => array(
'label' => T_('Items to compare'),
'type' => 'select',
'options' => array(
'all' => T_('All from collection'),
'param' => sprintf( T_('As specified by "%s" URL param'), 'items=' ),
'list' => T_('Specific IDs listed below'),
),
'defaultvalue' => 'all',
),
'items' => array(
'label' => T_('Specific Item IDs'),
'note' => sprintf( T_('Separate Item IDs or slugs or %s or %s with %s.'), '<code>$this$</code>', '<code>$parent$</code>', '<code>,</code>' ),
'valid_pattern' => array(
'pattern' => '/^(([\da-zA-Z\-_]+|\$this\$|\$parent\$)+(,([\da-zA-Z\-_]+|\$this\$|\$parent\$))*)?$/',
'error' => sprintf( T_('Items to compare must be specified by ID, by slug or as %s or %s.'), '<code>$this$</code>', '<code>$parent$</code>' ),
),
'size' => 80,
),
'items_type' => array(
'label' => T_('Restrict to Post Type'),
'type' => 'select',
'options' => $item_type_options,
'defaultvalue' => 'default',
),
'restrict_featured' => array(
'label' => T_('Restrict to featured'),
'type' => 'checkbox',
'defaultvalue' => 0,
),
'restrict_cats' => array(
'label' => T_('Restrict to Categories'),
'note' => sprintf( T_('List category IDs separated by %s.'), '<code>,</code>' ),
'defaultvalue' => '',
'size' => 80,
'valid_pattern' => array( 'pattern' => '/^(\d+(,\d+)*|-|\*)?$/',
'error' => T_('Invalid list of Category IDs.') ),
),
'restrict_tags' => array(
'label' => T_('Restrict to Tags'),
'note' => T_('Items must have ALL the tags listed here.'),
'type' => 'itemtag',
'defaultvalue' => '',
'size' => 80,
),
'items_limit' => array(
'label' => T_('Limit'),
'type' => 'integer',
'note' => T_('Max number of items that can be compared.'),
'defaultvalue' => 10,
'valid_range' => array(
'min' => 0,
),
'allow_empty' => true,
),
'allow_filter' => array(
'label' => T_('Allow filter params'),
'type' => 'checkbox',
'note' => sprintf( T_('Check to allow filtering/ordering with URL params such as %s etc.'), '<code>cat=</code>, <code>tag=</code>, <code>orderby=</code>' ),
'defaultvalue' => 0,
),
'display_condition' => array(
'label' => TB_('Show/Hide columns based on condition found in field'),
'size' => 50,
),
);
for( $order_index = 0; $order_index <= 2; $order_index++ )
{ // Default order settings:
$field_suffix = ( $order_index == 0 ? '' : '_'.$order_index );
$coll_item_sort_options = get_available_sort_options( $this->get( 'coll_ID' ), $order_index > 0, true );
$r = array_merge( $r, array(
'order_begin_line'.$field_suffix => array(
'type' => 'begin_line',
'label' => ( $order_index == 0 ? T_('Default order') : '' ),
),
'orderby'.$field_suffix => array(
'type' => 'select',
'options' => array_merge(
array( 'coll_default' => T_('Use collection\'s default'), ),
$coll_item_sort_options['general'],
array( T_('Custom fields') => $coll_item_sort_options['custom'] )
),
'defaultvalue' => 'coll_default',
),
'orderdir'.$field_suffix => array(
'type' => 'select',
'options' => array(
'ASC' => T_('Ascending'),
'DESC' => T_('Descending'),
),
'defaultvalue' => 'ASC',
),
'order_end_line'.$field_suffix => array(
'type' => 'end_line',
),
) );
}
$r = array_merge( $r, array(
'lines_layout_start' => array(
'layout' => 'begin_fieldset',
'label' => T_('Lines to show')
),
'show_headers' => array(
'type' => 'checkbox',
'label' => T_('Show column headers'),
'defaultvalue' => 1,
),
'merge_headers' => array(
'type' => 'checkbox',
'label' => T_('Auto merge'),
'note' => T_('Merge the column headers when they are identical.'),
'defaultvalue' => 0,
),
'show_status' => array(
'type' => 'radio',
'label' => T_('Show item visibility status'),
'options' => array(
array( 'always', T_('Always') ),
array( 'never', T_('Never') ),
array( 'differences', T_('Only if differences') ),
array( 'not_public', T_('Only if not public') ) ),
'defaultvalue' => 'not_public',
'field_lines' => true,
),
'fields_source' => array(
'label' => T_('Fields to show'),
'type' => 'select',
'options' => array(
'all' => T_('All'),
'exclude' => T_('All except fields listed below'),
'include' => T_('Only fields listed below'),
),
'defaultvalue' => 'all',
),
'fields' => array(
'type' => 'textarea',
'label' => '',
'note' => T_('Enter one field name per line.'),
'rows' => 10,
),
'cell_colors' => array(
'type' => 'checklist',
'label' => T_('Automatic cell colors'),
'options' => array(
array( 'diff', T_('Yellow highlights'), 1 ),
array( 'green', T_('Green highlights'), 1 ),
array( 'red', T_('Red highlights'), 1 ),
),
),
'hide_empty_lines' => array(
'type' => 'checkbox',
'label' => T_('Hide empty lines'),
'defaultvalue' => 1,
),
'edit_links' => array(
'type' => 'checkbox',
'label' => T_('Edit Links'),
'note' => T_('Check to add a row with edit buttons for editors who have permission.'),
'defaultvalue' => 1,
),
'lines_layout_end' => array(
'layout' => 'end_fieldset',
),
), parent::get_param_definitions( $params ) );
return $r;
}
/**
* Get advanced definitions for editable params.
*
* @see Plugin::GetDefaultSettings()
*
* @return array Advanced params
*/
function get_advanced_param_definitions()
{
return array(
'add_css' => array(
'type' => 'checklist',
'label' => T_('Add CSS classes to cells'),
'options' => array(
array( 'row', sprintf( T_('Identify rows with %s + field code'), '<code>comp_row_</code>' ), 0 ),
array( 'col', sprintf( T_('Identify columns with %s + Item slug'), '<code>comp_col_</code>' ), 0 ),
array( 'cat', sprintf( T_('Identify column category with %s + main Cat slug'), '<code>comp_cat_</code>' ), 0 ),
),
),
);
}
/**
* Display the widget!
*
* @param array MUST contain at least the basic display params
*/
function display( $params )
{
$this->init_display( $params );
$this->disp_params = array_merge( array(
'custom_fields_table_start' => '<div class="evo_content_block"><table class="item_custom_fields">',
'custom_fields_row_start' => '<tr$row_attrs$>',
'custom_fields_topleft_cell' => '<td style="border:none"></td>',
'custom_fields_col_header_item' => '<th class="$col_class$ center" width="$col_width$"$col_attrs$>$item_link$$item_status$</th>', // Note: we will also add reverse view later: 'custom_fields_col_header_field
'custom_fields_row_header_field' => '<th class="$header_cell_class$">$field_title$$field_description_icon$</th>',
'custom_fields_item_status_template' => '<div><div class="evo_status evo_status__$status$ badge" data-toggle="tooltip" data-placement="top" title="$tooltip_title$">$status_title$</div></div>',
'custom_fields_description_icon_class' => 'grey',
'custom_fields_value_default' => '<td class="$data_cell_class$"$data_cell_attrs$>$field_value$</td>',
'custom_fields_value_difference_highlight' => '<td class="$data_cell_class$ bg-warning"$data_cell_attrs$>$field_value$</td>',
'custom_fields_value_green' => '<td class="$data_cell_class$ bg-success"$data_cell_attrs$>$field_value$</td>',
'custom_fields_value_red' => '<td class="$data_cell_class$ bg-danger"$data_cell_attrs$>$field_value$</td>',
'custom_fields_edit_link_cell' => '<td class="center"$edit_link_attrs$>$edit_link$</td>',
'custom_fields_edit_link_class' => 'btn btn-xs btn-default',
'custom_fields_row_end' => '</tr>',
'custom_fields_table_end' => '</table></div>',
// Separate template for separator fields:
// (Possible to use templates for all field types: 'numeric', 'string', 'html', 'text', 'url', 'image', 'computed', 'separator')
'custom_fields_separator_row_header_field' => '<th class="$header_cell_class$" colspan="$cols_count$">$field_title$$field_description_icon$</th>',
), $this->disp_params );
// Get IDs of items which should be compared:
$items = $this->get_items_IDs();
if( empty( $items ) )
{ // No items to compare:
$this->display_debug_message( 'Widget "'.$this->get_name().'" is hidden because no Items to compare.' );
return false;
}
// Get custom fields with compared data:
$custom_fields = $this->get_custom_fields( $items );
if( empty( $custom_fields ) )
{ // No fields to compare:
$this->display_debug_message( 'Widget "'.$this->get_name().'" is hidden because no fields to compare.' );
return false;
}
// Check if headers for item statuses should be displayed
$show_status = $this->check_show_status( $items );
$ItemCache = & get_ItemCache();
echo $this->disp_params['block_start'];
$this->disp_title();
echo $this->disp_params['block_body_start'];
// Start a table to display differences of all custom fields for selected posts:
echo $this->get_field_template( 'table_start' );
if( $this->disp_params['show_headers'] || $show_status !== false )
{ // Display item column headers row if it is enabled by widget settings:
echo str_replace( '$row_attrs$', '', $this->get_field_template( 'row_start' ) );
echo $this->get_field_template( 'topleft_cell' );
$col_width = number_format( 100 / ( count( $items ) + 1 ), 2, '.', '' );
$table_header_cells = array();
foreach( $items as $i => $item_ID )
{
$widget_Item = & $ItemCache->get_by_ID( $item_ID, false, false );
if( $this->disp_params['show_headers'] )
{ // Get permanent item link:
$item_title_link = $widget_Item->get_title( array( 'title_field' => 'short_title,title' ) );
if( $this->disp_params['merge_headers'] )
{ // Get title text in order to compare on merging:
$item_title_text = $widget_Item->get_title( array( 'title_field' => 'short_title,title', 'link_type' => 'none' ) );
}
}
else
{ // Item title should not be displayed:
$item_title_link = '';
$item_title_text = '';
}
if( $show_status == 'always' ||
$show_status == 'differences' ||
( $show_status == 'not_public' && $widget_Item->get( 'status' ) != 'published' ) )
{ // Get item status:
$item_status_badge = $widget_Item->get_format_status( array( 'template' => $this->disp_params['custom_fields_item_status_template'] ) );
if( $this->disp_params['merge_headers'] )
{ // Get status text in order to compare on merging:
$item_status_text = $widget_Item->get( 'status' );
}
}
else
{ // Don't display item status:
$item_status_badge = '';
$item_status_text = '';
}
if( $this->disp_params['merge_headers'] )
{ // Check if previous header same as currect:
if( isset( $prev_item_title_text, $prev_item_status_text ) &&
$prev_item_title_text == $item_title_text &&
$prev_item_status_text == $item_status_text )
{ // This is a duplicated header cell as before:
$skip_duplicate_header_cell = true;
// Increase a count of duplicated hedaer cell:
$table_header_cells[ count( $table_header_cells ) - 1 ]['cols']++;
}
else
{ // Don't skip different column header cell:
$skip_duplicate_header_cell = false;
}
// Store current title values in order to comapre then next time:
$prev_item_title_text = $item_title_text;
$prev_item_status_text = $item_status_text;
}
if( empty( $skip_duplicate_header_cell ) )
{ // Display header cell only if it not hidden on merging same headers:
$col_class = '';
if( ! empty( $this->disp_params['add_css']['col'] ) )
{ // Add CSS class to identify columns with "comp_col_" + Item slug:
$col_class .= ' comp_col_'.$widget_Item->get( 'urltitle' );
}
if( ! empty( $this->disp_params['add_css']['cat'] ) &&
( $widget_main_Chapter = & $widget_Item->get_main_Chapter() ) )
{ // Add CSS class to identify column category with "comp_cat_" + main Cat slug:
$col_class .= ' comp_cat_'.$widget_main_Chapter->get( 'urlname' );
}
$col_class = trim( $col_class );
$table_header_cell = array(
'title' => $item_title_link,
'status' => $item_status_badge,
'class' => $col_class,
'cols' => 1,
);
if( ! empty( $this->disp_params['display_condition'] ) &&
( $display_condition = $widget_Item->get_custom_field_value( $this->disp_params['display_condition'] ) ) != '' )
{ // Use a display condition for column of the Item:
$table_header_cell['display_condition'] = $display_condition;
}
$table_header_cells[] = $table_header_cell;
}
}
foreach( $table_header_cells as $table_header_cell )
{ // Print out table header cells:
$cell_params = array(
'$item_link$' => $table_header_cell['title'],
'$item_status$' => $table_header_cell['status'],
'$col_class$' => $table_header_cell['class'],
'$col_width$' => ( $col_width * $table_header_cell['cols'] ).'%',
'$col_attrs$' => ( $table_header_cell['cols'] > 1 ? ' colspan="'.$table_header_cell['cols'].'"' : '' )
.( isset( $table_header_cell['display_condition'] ) ? $this->get_display_condition_attr( $table_header_cell['display_condition'], $items ) : '' ),
);
echo str_replace( array_keys( $cell_params ), $cell_params, $this->get_field_template( 'col_header_item' ) );
}
echo $this->get_field_template( 'row_end' );
}
foreach( $custom_fields as $custom_field )
{
if( $custom_field['display_mode'] == 'normal' )
{ // Display a row of one compared field between all selected items:
// Note: skip fields with display mode "repeat" which should be displayed after specific separator below:
$this->display_field_row_template( $custom_field, $items, $this->disp_params );
}
if( ! empty( $custom_field['repeat_fields'] ) )
{ // Display the repeated fields after separator if it has them:
foreach( $custom_field['repeat_fields'] as $repeat_field_name )
{ // Display a row of the repeated custom field between all selected items:
$this->display_field_row_template( $custom_fields[ $repeat_field_name ], $items, $this->disp_params );
}
}
}
if( $this->disp_params['edit_links'] && is_logged_in() )
{ // Display buttons to edit the compared items if user has a permission:
global $Item;
$items_edit_links = array();
$items_can_be_edited = false;
foreach( $items as $item_ID )
{
if( ! ( $widget_Item = & $ItemCache->get_by_ID( $item_ID, false, false ) ) )
{ // Skip wrong Item:
continue;
}
$item_edit_link_params = array();
if( isset( $Item ) && ( $Item instanceof Item ) && $Item->ID == $item_ID && count( $items ) == 1 )
{ // Don't display an edit link when this is a page of currently displayed Item:
$item_edit_link_params['link'] = '';
}
else
{ // Try to display an edit link depending on permissions of current User:
$item_edit_link = $widget_Item->get_edit_link( array( 'class' => $this->disp_params['custom_fields_edit_link_class'] ) );
if( $item_edit_link === false )
{ // The edit link is not available for current User:
$item_edit_link_params['link'] = '';
}
else
{ // The Item can be edited by current User:
$item_edit_link_params['link'] = $item_edit_link;
// Set flag to know at least one compared item can be edited by current User:
$items_can_be_edited = true;
}
}
if( ! empty( $this->disp_params['display_condition'] ) &&
( $display_condition = $widget_Item->get_custom_field_value( $this->disp_params['display_condition'] ) ) != '' )
{ // Use a display condition for column of the Item:
$item_edit_link_params['display_condition'] = $display_condition;
}
$items_edit_links[] = $item_edit_link_params;
}
if( $items_can_be_edited )
{ // Display the footer row with edit links if at least one compared item can be edited by current User:
echo str_replace( '$row_attrs$', '', $this->get_field_template( 'row_start' ) );
echo $this->get_field_template( 'topleft_cell' );
foreach( $items_edit_links as $items_edit_link )
{
$item_edit_cell_params = array(
'$edit_link$' => $items_edit_link['link'],
'$edit_link_attrs$' => ( isset( $items_edit_link['display_condition'] ) ? $this->get_display_condition_attr( $items_edit_link['display_condition'], $items ) : '' ),
);
echo str_replace( array_keys( $item_edit_cell_params ), $item_edit_cell_params, $this->get_field_template( 'edit_link_cell' ) );
}
echo $this->get_field_template( 'row_end' );
}
}
echo $this->get_field_template( 'table_end' );
echo $this->disp_params['block_body_end'];
echo $this->disp_params['block_end'];
return true;
}
/**
* Get custom fields between the requested items with additional compare data
*
* @param array Item IDs, updated by reference
* @return array
*/
function get_custom_fields( & $items )
{
global $Item;
if( empty( $items ) )
{ // No items to compare:
return false;
}
$ItemCache = & get_ItemCache();
// Load all requested posts into the cache:
$ItemCache->clear();
$ItemCache->load_list( $items );
$fields_source = $this->disp_params['fields_source'];
if( $fields_source == 'exclude' || $fields_source == 'include' )
{ // Check what fields should be excluded/included for this widget:
$widget_fields = trim( $this->disp_params['fields'] );
$widget_fields = empty( $widget_fields ) ? array() : preg_split( '#[\s\n\r]+#', $widget_fields );
}
$all_custom_fields = array();
foreach( $items as $i => $item_ID )
{
if( ! $widget_Item = & $ItemCache->get_by_ID( $item_ID, false, false ) )
{ // Skin wrong post:
unset( $items[ $i ] );
continue;
}
if( ! empty( $Item ) && $Item->ID == $widget_Item->ID && $Item->is_revision() )
{ // Display data from requested revision of the Item:
$widget_Item->set( 'revision', $Item->revision );
}
// Load all custom fields of the Item in cache:
$item_custom_fields = $widget_Item->get_custom_fields_defs();
// Use either all custom fields of the Item or only specified fields in config of this Widget:
switch( $fields_source )
{
case 'exclude':
// Exclude custom fields from the specific list:
$search_custom_fields = array_diff( array_keys( $item_custom_fields ), $widget_fields );
break;
case 'include':
// Include custom fields from the specific list:
$search_custom_fields = $widget_fields;
break;
case 'all':
default:
// Use all custom fields:
$search_custom_fields = array_keys( $item_custom_fields );
break;
}
foreach( $search_custom_fields as $search_custom_field_key )
{
$search_custom_field_name = $search_custom_field_key;
$field_options = '';
if( $fields_source == 'include' && strpos( $search_custom_field_key, '+' ) !== false )
{ // Parse additional field options, e.g. separators may have names as 'separator+repeat', 'separator+fields', 'separator+repeat+fields':
$search_custom_field_options = explode( '+', $search_custom_field_key, 2 );
if( isset( $search_custom_field_options[1] ) )
{ // The field has additional options:
$field_options = $search_custom_field_options[1];
}
// Set real name of separator field from key like 'separator+repeat+fields':
$search_custom_field_name = $search_custom_field_options[0];
}
if( ! isset( $all_custom_fields[ $search_custom_field_name ] ) )
{ // Initialize array to keep fields in the requested order:
$all_custom_fields[ $search_custom_field_name ] = array();
}
if( ! isset( $item_custom_fields[ $search_custom_field_name ] ) )
{ // Skip because the post has no this custom field:
continue;
}
$item_custom_field = $item_custom_fields[ $search_custom_field_name ];
if( ! $item_custom_field['public'] )
{ // Skip not public custom field:
continue;
}
if( isset( $all_custom_fields[ $search_custom_field_key ]['display_mode'] ) &&
$all_custom_fields[ $search_custom_field_key ]['display_mode'] == 'repeat' )
{ // Reinitialize custom field with correct order if it was initialized before as repeat field of some separator above this custom field:
unset( $all_custom_fields[ $search_custom_field_key ] );
}
if( empty( $all_custom_fields[ $search_custom_field_key ] ) )
{ // Initialize array to store items which really have this custom field:
$all_custom_fields[ $search_custom_field_key ] = $item_custom_field;
$all_custom_fields[ $search_custom_field_key ]['display_mode'] = 'normal';
$all_custom_fields[ $search_custom_field_key ]['items'] = array();
}
if( ! in_array( $item_ID , $all_custom_fields[ $search_custom_field_key ]['items'] ) )
{ // Store ID of the post which has this custom field:
$all_custom_fields[ $search_custom_field_key ]['items'][] = $item_ID;
}
if( $item_custom_field['type'] == 'separator' )
{ // Initialize the repeat fields and fields under separator field until next separtor:
if( ! empty( $item_custom_field['format'] ) &&
( strpos( $field_options, 'repeat' ) !== false || // if field is requested with name like 'separator+repeat' or 'separator+repeat+fields'
$fields_source != 'include' // also get all repeat fields when fields list is full
)
)
{ // Try to find the repeat fields:
$separator_format = explode( ':', $item_custom_field['format'] );
if( $separator_format[0] != 'repeat' || empty( $separator_format[1] ) )
{ // Skip wrong separator format:
continue;
}
$repeat_fields = explode( ',', $separator_format[1] );
}
else
{ // The separator has no repeat fields:
$repeat_fields = array();
}
if( $fields_source == 'include' &&
strpos( $field_options, 'fields' ) !== false ) // if field is requested with name like 'separator+fields' or 'separator+repeat+fields'
{ // Try to find fields under separator only when we request a specific fields list,
// in full list the fields under separator are displayed automatically, se we should not get them to avoid duplicated view:
$is_under_separator_field = false;
foreach( $item_custom_fields as $ic_field_name => $ic_field )
{
if( $ic_field_name == $search_custom_field_name )
{ // We found the current separtor, set flag to use next fields:
$is_under_separator_field = true;
continue;
}
if( $is_under_separator_field )
{ // This is a field under current separator:
if( $ic_field['type'] == 'separator' )
{ // Stop here because it is another separator:
break;
}
$repeat_fields[] = $ic_field_name;
}
}
}
foreach( $repeat_fields as $r => $repeat_field_name )
{
$repeat_field_name = trim( $repeat_field_name );
if( ! isset( $item_custom_fields[ $repeat_field_name ] ) ||
! $item_custom_fields[ $repeat_field_name ]['public'] )
{ // Skip unknown or not public field:
unset( $repeat_fields[ $r ] );
continue;
}
$repeat_fields[ $r ] = $repeat_field_name;
$item_custom_field = $item_custom_fields[ $repeat_field_name ];
if( empty( $all_custom_fields[ $repeat_field_name ] ) )
{ // Initialize array to store items which really have this custom field:
$all_custom_fields[ $repeat_field_name ] = $item_custom_field;
$all_custom_fields[ $repeat_field_name ]['display_mode'] = 'repeat'; // Special display mode in order to display this only after the separator
$all_custom_fields[ $repeat_field_name ]['items'] = array();
}
if( ! in_array( $item_ID , $all_custom_fields[ $repeat_field_name ]['items'] ) )
{ // Store ID of the post which has this custom field:
$all_custom_fields[ $repeat_field_name ]['items'][] = $item_ID;
}
}
if( ! isset( $all_custom_fields[ $search_custom_field_key ]['repeat_fields'] ) &&
! empty( $repeat_fields ) )
{ // Initialize array to store the repeat fields of the separator:
$all_custom_fields[ $search_custom_field_key ]['repeat_fields'] = $repeat_fields;
}
}
}
}
// Clear array to remove fields which are not used by any post,
// for case when field doesn't exist or not public:
foreach( $all_custom_fields as $a => $all_custom_field )
{
if( empty( $all_custom_field ) )
{
unset( $all_custom_fields[ $a ] );
}
}
if( $fields_source == 'all' || $fields_source == 'exclude' )
{ // Sort custom fields from all requested posts by custom field order:
uasort( $all_custom_fields, array( $this, 'sort_custom_fields' ) );
}
if( empty( $all_custom_fields ) )
{ // Don't display widget if all selected items have no custom fields:
return false;
}
// Compare custom field values:
$items_count = count( $items );
foreach( $all_custom_fields as $c => $custom_field )
{
if( $custom_field['type'] == 'separator' )
{ // Separator fields have no values:
continue;
}
$all_custom_fields[ $c ]['is_different'] = false;
$is_numeric_type = in_array( $custom_field['type'], array( 'double', 'computed' ) );
if( $is_numeric_type )
{ // Compare only numeric fields:
$all_custom_fields[ $c ]['highest_value'] = NULL;
$all_custom_fields[ $c ]['lowest_value'] = NULL;
}
if( isset( $this->disp_params['cell_colors']['diff'] ) &&
$items_count != count( $custom_field['items'] ) )
{ // If some post has no field then it is a different:
$all_custom_fields[ $c ]['is_different'] = true;
}
// Check for empty all values from this line only it is required by widget setting:
$this_line_values_are_empty = $this->disp_params['hide_empty_lines'];
// Compare values:
$prev_custom_field_value = NULL;
$i = 0;
foreach( $custom_field['items'] as $item_ID )
{
$widget_Item = & $ItemCache->get_by_ID( $item_ID, false, false );
$custom_field_value = $widget_Item->get_custom_field_value( $custom_field['name'] );
if( $this_line_values_are_empty &&
( $custom_field_value !== NULL || $custom_field['type'] == 'separator' ) )
{ // At least one field is not empty:
$this_line_values_are_empty = false;
}
// Check if the values are different from given line:
if( isset( $this->disp_params['cell_colors']['diff'] ) &&
! $all_custom_fields[ $c ]['is_different'] &&
$i > 0 )
{ // Don't search differences in all fields if at least two fields are different:
switch( $custom_field['type'] )
{
case 'image':
// Special comparing for image fields:
$LinkCache = & get_LinkCache();
if( $prev_Link = & $LinkCache->get_by_ID( $prev_custom_field_value, false, false ) &&
$prev_File = & $prev_Link->get_File() &&
$cur_Link = & $LinkCache->get_by_ID( $custom_field_value, false, false ) &&
$cur_File = & $cur_Link->get_File() )
{ // Compare hashes of the two files:
$is_different = $prev_File->get( 'hash' ) != $cur_File->get( 'hash' );
}
else
{ // If at least one field has a wrong link ID then compare IDs:
$is_different = $custom_field_value != $prev_custom_field_value;
}
break;
default:
$is_different = $custom_field_value != $prev_custom_field_value;
break;
}
if( $is_different )
{ // Mark this field is different:
$all_custom_fields[ $c ]['is_different'] = true;
}
}
$prev_custom_field_value = $custom_field_value;
if( $is_numeric_type && is_numeric( $custom_field_value ) )
{ // Compare only numeric values:
// Search the highest value:
if( $all_custom_fields[ $c ]['highest_value'] === NULL ||
$custom_field_value > $all_custom_fields[ $c ]['highest_value'] )
{
$all_custom_fields[ $c ]['highest_value'] = $custom_field_value;
}
// Search the lowest value:
if( $all_custom_fields[ $c ]['lowest_value'] === NULL ||
$custom_field_value < $all_custom_fields[ $c ]['lowest_value'] )
{
$all_custom_fields[ $c ]['lowest_value'] = $custom_field_value;
}
}
$i++;
}
if( $this_line_values_are_empty )
{ // Don't display row/line of custom field if values from all compared items are empty and if it is required by widget setting to hide empty lines:
unset( $all_custom_fields[ $c ] );
}
}
return $all_custom_fields;
}
/**
* Display a row of one compared field between the requested items
*
* @param array Custom field data
* @param array IDs of the compared items
* @param array Additional parameters
*/
function display_field_row_template( $custom_field, $items, $params = array() )
{
echo str_replace( '$row_attrs$', $this->get_display_condition_attr( $custom_field['disp_condition'], $items ), $this->get_field_template( 'row_start', $custom_field['type'] ) );
if( empty( $custom_field['description'] ) )
{ // The custom field has no description:
$field_description_icon = '';
}
else
{ // Display a description in tooltip of the help icon:
$field_description_icon = ' '.get_icon( 'help', 'imgtag', array(
'data-toggle' => 'tooltip',
'title' => nl2br( $custom_field['description'] ),
'class' => $params['custom_fields_description_icon_class'],
) ).' ';
}
// Render special masks like #yes#, (+), #stars/3# and etc. in value with template:
$custom_field_label = render_custom_field( $custom_field['label'], $params );
$header_class = $custom_field['header_class'];
if( ! empty( $this->disp_params['add_css']['row'] ) )
{ // Add CSS class to identify row with "comp_row_" + field code:
$header_class .= ' comp_row_'.$custom_field['name'];
}
$header_class = trim( $header_class );
// Custom field title:
echo str_replace( array( '$field_title$', '$cols_count$', '$field_description_icon$', '$header_cell_class$' ),
array( $custom_field_label, count( $items ) + 1, $field_description_icon, $header_class ),
$this->get_field_template( 'row_header_field', $custom_field['type'] ) );
if( $custom_field['type'] != 'separator' )
{ // Separator fields have no values:
$table_row_cells = array();
$ItemCache = & get_ItemCache();
foreach( $items as $item_ID )
{
if( ! ( $widget_Item = & $ItemCache->get_by_ID( $item_ID, false, false ) ) )
{ // Skip wrong Item:
continue;
}
// Custom field value per each post:
if( in_array( $item_ID, $custom_field['items'] ) )
{ // Get a formatted value if post has this custom field:
$custom_field_value = $widget_Item->get_custom_field_formatted( $custom_field['name'], $params );
$custom_field_orig_value = $widget_Item->get_custom_field_value( $custom_field['name'] );
}
else
{ // This post has no this custom field:
$custom_field_value = '';
$custom_field_orig_value = false;
}
// Default template for field value:
$field_value_template = $this->get_field_template( 'value_default', $custom_field['type'] );
if( isset( $this->disp_params['cell_colors']['diff'] ) &&
( ( $custom_field['is_different'] && $custom_field['line_highlight'] == 'differences' ) ||
( $custom_field['line_highlight'] == 'always' && count( $items ) > 1 ) ) )
{ // Mark the field value as different only when it is defined in the settings of the custom field:
$field_value_template = $this->get_field_template( 'value_difference_highlight', $custom_field['type'] );
}
if( in_array( $custom_field['type'], array( 'double', 'computed' ) ) &&
is_numeric( $custom_field_orig_value ) )
{ // Compare only numeric values:
if( $custom_field_orig_value === $custom_field['highest_value'] &&
$custom_field_orig_value !== $custom_field['lowest_value'] )
{ // Check if we should mark the highest field:
if( isset( $this->disp_params['cell_colors']['green'] ) &&
$custom_field['green_highlight'] == 'highest' )
{ // The highest value must be marked as green:
$field_value_template = $this->get_field_template( 'value_green', $custom_field['type'] );
}
elseif( isset( $this->disp_params['cell_colors']['red'] ) &&
$custom_field['red_highlight'] == 'highest' )
{ // The highest value must be marked as red:
$field_value_template = $this->get_field_template( 'value_red', $custom_field['type'] );
}
}
if( $custom_field_orig_value === $custom_field['lowest_value'] &&
$custom_field_orig_value !== $custom_field['highest_value'] )
{ // Check if we should mark the lowest field:
if( isset( $this->disp_params['cell_colors']['green'] ) &&
$custom_field['green_highlight'] == 'lowest' )
{ // The lowest value must be marked as green:
$field_value_template = $this->get_field_template( 'value_green', $custom_field['type'] );
}
elseif( isset( $this->disp_params['cell_colors']['red'] ) &&
$custom_field['red_highlight'] == 'lowest' )
{ // The lowest value must be marked as red:
$field_value_template = $this->get_field_template( 'value_red', $custom_field['type'] );
}
}
}
if( $custom_field['merge'] )
{ // Check if previous field value same as currect:
if( isset( $prev_field_value ) && $prev_field_value == $custom_field_value )
{ // This is a duplicated field value cell as before:
$skip_duplicate_field_value = true;
// Increase a count of duplicated field value cell:
$table_row_cells[ count( $table_row_cells ) - 1 ]['cols']++;
}
else
{ // Don't skip different field value cell:
$skip_duplicate_field_value = false;
}
// Store current field value in order to comapre then next time:
$prev_field_value = $custom_field_value;
}
$cell_class = $custom_field['cell_class'];
if( ! empty( $this->disp_params['add_css']['row'] ) )
{ // Add CSS class to identify row with "comp_row_" + field code:
$cell_class .= ' comp_row_'.$custom_field['name'];
}
if( ! empty( $this->disp_params['add_css']['col'] ) )
{ // Add CSS class to identify columns with "comp_col_" + Item slug:
$cell_class .= ' comp_col_'.$widget_Item->get( 'urltitle' );
}
if( ! empty( $this->disp_params['add_css']['cat'] ) &&
( $widget_main_Chapter = & $widget_Item->get_main_Chapter() ) )
{ // Add CSS class to identify column category with "comp_cat_" + main Cat slug:
$cell_class .= ' comp_cat_'.$widget_main_Chapter->get( 'urlname' );
}
$cell_class = trim( $cell_class );
if( empty( $skip_duplicate_field_value ) )
{ // Display field value cell only if it not hidden on merging same values:
$table_row_cell = array(
'template' => str_replace( array( '$data_cell_class$', '$field_value$' ), array( $cell_class, $custom_field_value ), $field_value_template ),
'cols' => 1,
);
if( ! empty( $this->disp_params['display_condition'] ) &&
( $display_condition = $widget_Item->get_custom_field_value( $this->disp_params['display_condition'] ) ) != '' )
{ // Use a display condition for column of the Item:
$table_row_cell['display_condition'] = $display_condition;
}
$table_row_cells[] = $table_row_cell;
}
}
foreach( $table_row_cells as $table_row_cell )
{ // Print out table field value cells:
echo str_replace( '$data_cell_attrs$',
( $table_row_cell['cols'] > 1 ? ' colspan="'.$table_row_cell['cols'].'"' : '' )
.( isset( $table_row_cell['display_condition'] ) ? $this->get_display_condition_attr( $table_row_cell['display_condition'], $items ) : '' ),
$table_row_cell['template'] );
}
}
echo $this->get_field_template( 'row_end', $custom_field['type'] );
}
/**
* Get IDs of items which are used by this widget depending on settings
*
* @return array
*/
function get_items_IDs()
{
global $Collection, $Blog, $Item;
switch( $this->disp_params['items_source'] )
{
case 'all':
// Use all items from current collection,
// They are loaded by ItemList below:
$items = 'all';
break;
case 'param':
// Use items from param:
$items = param( 'items', '/^[\d,]*$/' );
$items = trim( $items, ',' );
$items = empty( $items ) ? false : explode( ',', $items );
break;
case 'list':
// Use items from specific list:
$items = trim( $this->disp_params['items'], ',' );
$items = empty( $items ) ? false : explode( ',', $items );
break;
default:
// Stop here, because unknown items source.
return array();
}
if( empty( $items ) && $items != 'all' )
{ // No items to compare:
return array();
}
$ItemCache = & get_ItemCache();
$items_limit = intval( $this->disp_params['items_limit'] );
$items_limit = $items_limit > 0 ? $items_limit : NULL;
if( is_array( $items ) )
{ // Check item IDs which are loaded from URL param 'items=' or from specific widget settings list:
foreach( $items as $i => $item_ID )
{
$item_ID = trim( $item_ID );
if( empty( $item_ID ) )
{ // Remove wrong item ID:
unset( $items[ $i ] );
}
if( $item_ID == '$this$' )
{ // Try to get a current post ID:
if( isset( $Item ) && $Item instanceof Item )
{ // Use ID of current post:
$items[ $i ] = $Item->ID;
}
else
{ // Remove it because no current post:
unset( $items[ $i ] );
}
}
elseif( $item_ID == '$parent$' )
{ // Try to get a parent post ID:
if( isset( $Item ) && $Item instanceof Item && $Item->get( 'parent_ID' ) > 0 )
{ // Use ID of parent post:
$items[ $i ] = $Item->get( 'parent_ID' );
}
else
{ // Remove it because no parent post:
unset( $items[ $i ] );
}
}
}
// Load all requested Items by single SQL query into cache:
$ItemCache->load_by_IDs_or_slugs( $items );
foreach( $items as $i => $item_ID )
{
if( ! is_number( $item_ID ) )
{ // Try to get a post ID by slug:
if( $widget_Item = & $ItemCache->get_by_urltitle( $item_ID, false, false ) )
{ // Use ID of post detected by slug:
$items[ $i ] = $widget_Item->ID;
}
else
{ // Remove it because cannot find post by slug:
unset( $items[ $i ] );
}
}
}
// Remove duplicated items with same ID:
$items = array_unique( $items );
// Save original orders of the items when they are defined as specific list:
$orig_ordered_items = $items;
}
if( empty( $Blog ) )
{ // Cannot use filter by ItemList below because current collection is not defined:
return $items;
}
// Use ItemList in order to check what items can be displayed on front-office for current User
// OR in order to filter items if it is required by widget setting:
$ItemList = new ItemList2( $Blog, $Blog->get_timestamp_min(), $Blog->get_timestamp_max(), $items_limit );
// Set additional debug info prefix for SQL queries in order to know what code executes it:
$ItemList->query_title_prefix = get_class().' #'.$this->ID;
if( $this->disp_params['items_type'] == 'default' )
{ // Exclude items with types which are hidden by collection setting "Show post types":
$filter_item_type = $Blog->get_setting( 'show_post_types' ) != '' ? '-'.$Blog->get_setting( 'show_post_types' ) : NULL;
}
else
{ // Filter by selected Item Type:
$filter_item_type = intval( $this->disp_params['items_type'] );
}
// Set default orders:
$default_orders = array();
$default_dirs = array();
for( $order_index = 0; $order_index <= 2; $order_index++ )
{
$field_suffix = ( $order_index == 0 ? '' : '_'.$order_index );
$widget_orderby = $this->disp_params['orderby'.$field_suffix];
if( $widget_orderby == 'coll_default' )
{ // Use order from collection:
$coll_orderby = $Blog->get_setting( 'orderby'.$field_suffix );
if( ! empty( $coll_orderby ) )
{
$default_orders[] = $coll_orderby;
$default_dirs[] = $Blog->get_setting( 'orderdir'.$field_suffix );
}
}
elseif( ! empty( $widget_orderby ) )
{ // Use order from widget settings:
$default_orders[] = $widget_orderby;
$default_dirs[] = $this->disp_params['orderdir'.$field_suffix];
}
}
// Set default filters:
$default_filters = array(
'types' => $filter_item_type,
'post_ID_list' => is_array( $items ) ? implode( ',', $items ) : NULL,
'orderby' => implode( ',', $default_orders ),
'order' => implode( ',', $default_dirs ),
'featured' => ( $this->disp_params['restrict_featured'] ? true : NULL ),
);
if( ! empty( $this->disp_params['restrict_cats'] ) )
{ // Restrict by categories:
$default_filters['cat_array'] = explode( ',', $this->disp_params['restrict_cats'] );
}
if( ! empty( $this->disp_params['restrict_tags'] ) )
{ // Restrict by tags:
$default_filters['tags'] = $this->disp_params['restrict_tags'];
$default_filters['tags_operator'] = 'AND';
}
$ItemList->set_default_filters( $default_filters );
if( $this->disp_params['allow_filter'] )
{ // Filter items from request:
$ItemList->load_from_Request( false );
}
// Run query:
$ItemList->query();
// Get IDs of items filtered by $ItemList:
$items = $ItemList->get_page_ID_array();
if( isset( $orig_ordered_items ) && count( $items ) )
{ // Revert original orders of items:
$fix_ordered_items = array();
foreach( $orig_ordered_items as $orig_ordered_item_ID )
{
if( ( $item_ID_index = array_search( $orig_ordered_item_ID, $items ) ) !== false )
{
$fix_ordered_items[] = $items[ $item_ID_index ];
}
}
// Replace items ordered by $ItemList with original ordered array:
$items = $fix_ordered_items;
}
return $items;
}
/**
* Get field template depending on type of the custom field
*
* @param string Template name
* @param string Custom field type: 'double', 'varchar', 'html', 'text', 'url', 'image', 'computed', 'separator'
* @return string HTML template
*/
function get_field_template( $template_name, $field_type = '' )
{
// Convert field types to non-devs names:
$field_type = ( $field_type == 'double' ? 'numeric' : ( $field_type == 'varchar' ? 'string' : $field_type ) );
if( isset( $this->disp_params['custom_fields_'.$field_type.'_'.$template_name] ) )
{ // Use special template for current type if it is defined:
return $this->disp_params['custom_fields_'.$field_type.'_'.$template_name];
}
elseif( isset( $this->disp_params['custom_fields_'.$template_name] ) )
{ // Use generic template for all types:
return $this->disp_params['custom_fields_'.$template_name];
}
// Unknown template:
return '';
}
/**
* Callback function to sort custom fields by order field
*
* @param array Custom field A
* @param array Custom field B
* @return boolean
*/
function sort_custom_fields( $custom_field_a, $custom_field_b )
{
return $custom_field_a['order'] > $custom_field_b['order'];
}
/**
* Check if headers for item statuses should be displayed
*
* @param array Item IDs
* @return boolean|string FALSE when all statuses should not be displayed, 'always', 'differences', 'not_public'
*/
function check_show_status( $items )
{
switch( $this->disp_params['show_status'] )
{
case 'always':
// All statuses should be displayed:
return $this->disp_params['show_status'];
case 'differences':
// Check when at least one status should be displayed:
$diff_item_statuses = array();
$ItemCache = & get_ItemCache();
foreach( $items as $item_ID )
{
$widget_Item = & $ItemCache->get_by_ID( $item_ID, false, false );
$diff_item_statuses[ $widget_Item->get( 'status' ) ] = true;
if( count( $diff_item_statuses ) > 1 )
{ // When we find second different status we can stop here:
return $this->disp_params['show_status'];
}
}
break;
case 'not_public':
// Check when at least one status should be displayed:
$ItemCache = & get_ItemCache();
foreach( $items as $item_ID )
{
$widget_Item = & $ItemCache->get_by_ID( $item_ID, false, false );
if( $widget_Item->get( 'status' ) != 'published' )
{ // We found first status which should be displayed, stop search others:
return $this->disp_params['show_status'];
}
}
break;
}
// Statuses are never displayed or no items with expecting statuses:
return false;
}
/**
* Maybe be overriden by some widgets, depending on what THEY depend on..
*
* @return array of keys this widget depends on
*/
function get_cache_keys()
{
global $Collection, $Blog, $Item;
// Get IDs of items which should be compared:
$items = $this->get_items_IDs();
$cache_keys = array(
'wi_ID' => $this->ID, // Have the widget settings changed ?
'set_coll_ID' => $Blog->ID, // Have the settings of the blog changed ? (ex: new skin)
'item_ID' => isset( $Item ) && ( $Item instanceof Item ) ? $Item->ID : NULL, // Has the Item page changed? (this is important for disp=single|page because $this$ and $parent$ resolve differently depending on item ID)
'items' => implode( ',', $items ), // Have the compared items changed? (Check firstly widget setting and then param from request) (this is important in case the same items are compared in different order)
);
if( $this->disp_params['edit_links'] )
{ // When the edit links setting is enabled we should invalidate keys per user,
// because each user may has different permissions to edit the compared posts:
global $current_User;
$cache_keys['user_ID'] = ( is_logged_in() ? $current_User->ID : 0 ); // Has the current User changed?
}
$ItemCache = & get_ItemCache();
// Add 1 cache key for each item that is being compared, in order to detect changes on each one:
// Also add 1 cache key for item type which is used for compared items, in order to detect changes on each one:
foreach( $items as $item_ID )
{
// 1 is a dummy value, only the key name is really important
$cache_keys['item_'.$item_ID] = 1;
if( $Item = & $ItemCache->get_by_ID( $item_ID, false, false ) )
{ // Add cache key for item type of the compared item:
$cache_keys['item_type_'.$Item->get( 'ityp_ID' )] = 1;
}
}
return $cache_keys;
}
/**
* Get HTML attribute for display condition
*
* @param string Condition, e.g. cur=usd&dur=1mo
* @param array Items IDs
* @return string HTML attribute, e.g. ' data-display-condition="cur=usd&dur=1mo" style="display:none"'
*/
function get_display_condition_attr( $condition, $items = array() )
{
if( $condition == '' )
{ // No display condition:
return '';
}
// Set additional params for display condition:
$attrs = ' data-display-condition="'.format_to_output( $condition, 'htmlattr' ).'"';
// Load switchable params of all compared Items in order to initialize default values:
$ItemCache = & get_ItemCache();
foreach( $items as $item_ID )
{
if( $widget_Item = & $ItemCache->get_by_ID( $item_ID, false, false ) )
{ // If Item is detected:
$widget_Item->load_switchable_params();
}
}
// Check current params:
$disp_conditions = explode( '&', $condition );
foreach( $disp_conditions as $disp_condition )
{
$disp_condition = explode( '=', $disp_condition );
// Get all allowed value by the condition of the custom field:
$disp_condition_values = isset( $disp_condition[1] ) ? explode( '|', $disp_condition[1] ) : array( '' );
// Get current value of the param from $_GET or $_POST:
$param_value = param( $disp_condition[0], 'string' );
// Check if we should hide the custom field by condition:
if( ( $param_value === '' && ! in_array( '', $disp_condition_values ) ) || // current param value is empty but condition doesn't allow empty values
! preg_match( '/^[a-z0-9_\-]*$/', $param_value ) || // wrong param value
! in_array( $param_value, $disp_condition_values ) ) // current param value is not allowed by the condition of the custom field
{ // Hide custom field if at least one param is not allowed by condition of the custom field:
$attrs .= ' style="display:none"';
break;
}
}
return $attrs;
}
}
?>