Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

File diff suppressed because it is too large. Load diff
+30 -21
View File
@@ -11,47 +11,56 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Http\Url;
use IPS\Request;
use function array_slice;
use function count;
use function defined;
use function in_array;
use function is_callable;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* List Table Builder using an array datasource
*/
class _Custom extends Table
class Custom extends Table
{
/**
* @brief Data
*/
protected $dataSource;
protected array $dataSource;
/**
* @brief Parse Source
*/
public $parseSource = TRUE;
public bool $parseSource = TRUE;
/**
* Number of results
*/
public $count = 0;
public int $count = 0;
/**
* Number of pages
*/
public $pages = NULL;
public int $pages = 0;
/**
* Constructor
*
* @param array $dataSource Data source
* @param \IPS\Http\Url $baseUrl Base URL
* @param array $dataSource Data source
* @param Url $baseUrl Base URL
*/
public function __construct( $dataSource, \IPS\Http\Url $baseUrl, $count = NULL )
public function __construct( array $dataSource, Url $baseUrl, int $count = NULL )
{
$this->dataSource = $dataSource;
$this->count = \count( $this->dataSource );
$this->count = count( $this->dataSource );
return parent::__construct( $baseUrl );
}
@@ -59,11 +68,11 @@ class _Custom extends Table
/**
* Get rows
*
* @param array $advancedSearchValues Values from the advanced search form
* @param array|null $advancedSearchValues Values from the advanced search form
* @note $advancedSearchValues is currently ignored
* @return array
*/
public function getRows( $advancedSearchValues )
public function getRows( array $advancedSearchValues = NULL ): array
{
if ( ! $this->pages )
{
@@ -74,13 +83,13 @@ class _Custom extends Table
$rows = array();
foreach ( $this->dataSource as $i => $data )
{
$row = $this->include ? array_combine( $this->include, array_fill( 0, \count( $this->include ), NULL ) ) : array();
$row = $this->include ? array_combine( $this->include, array_fill( 0, count( $this->include ), NULL ) ) : array();
/* Get the columns from the XML */
foreach ( $data as $k => $v )
{
/* Are we including this one? */
if( ( $this->include !== NULL and !\in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and \in_array( $k, $this->exclude ) ) )
if( ( $this->include !== NULL and !in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and in_array( $k, $this->exclude ) ) )
{
continue;
}
@@ -94,15 +103,15 @@ class _Custom extends Table
}
/* Quicksearch */
if ( $this->quickSearch !== NULL and \IPS\Request::i()->quicksearch )
if ( $this->quickSearch !== NULL and Request::i()->quicksearch )
{
$quickSearchColumn = $this->quickSearch;
$rows = array_filter( $rows, \is_callable( $this->quickSearch ) ? $this->quickSearch : function( $row ) use ( $quickSearchColumn )
$rows = array_filter( $rows, is_callable( $this->quickSearch ) ? $this->quickSearch : function( $row ) use ( $quickSearchColumn )
{
return mb_strpos( mb_strtolower( $row[ $quickSearchColumn ] ), mb_strtolower( trim( \IPS\Request::i()->quicksearch ) ) ) !== FALSE;
return mb_strpos( mb_strtolower( $row[ $quickSearchColumn ] ), mb_strtolower( trim( Request::i()->quicksearch ) ) ) !== FALSE;
} );
$this->count = \count( $rows );
$this->count = count( $rows );
$this->pages = ceil( $this->count / $this->limit );
}
@@ -127,7 +136,7 @@ class _Custom extends Table
/* Limit */
if ( $this->limit and $this->count > $this->limit and $this->parseSource )
{
$rows = \array_slice( $rows, ( $this->limit * ( $this->page - 1 ) ), $this->limit, TRUE );
$rows = array_slice( $rows, ( $this->limit * ( $this->page - 1 ) ), $this->limit, TRUE );
}
/* Apply parsers */
@@ -141,7 +150,7 @@ class _Custom extends Table
$parserFunction = $this->parsers[ $k ];
$v = $parserFunction( $v, $this->dataSource[ $i ] );
}
else
else if ( is_string( $v ) )
{
$v = htmlspecialchars( $v, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );
}
+86 -67
View File
@@ -11,83 +11,99 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateTimeZone;
use Exception;
use IPS\DateTime;
use IPS\Db as IPSDb;
use IPS\Http\Url;
use IPS\Member;
use IPS\Request;
use OutOfRangeException;
use UnderflowException;
use function count;
use function defined;
use function in_array;
use function is_array;
use function is_callable;
use function is_object;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* List Table Builder using a database table datasource
*/
class _Db extends Table
class Db extends Table
{
/**
* @brief Database Table
*/
protected $table;
protected ?string $table = NULL;
/**
* @brief Selects
*/
public $selects = array();
public array $selects = array();
/**
* @brief Initial WHERE clause
*/
public $where;
public mixed $where = NULL;
/**
* @brief Force index clause
*/
protected $index;
protected mixed $index = NULL;
/**
* @brief Primary sort column
*/
public $primarySortBy;
public ?string $primarySortBy = NULL;
/**
* @brief Direction of primary sort
*/
public $primarySortDirection;
public ?string $primarySortDirection = NULL;
/**
* @brief Joins
*/
public $joins = array();
public array $joins = array();
/**
* @brief Key field
*/
public $keyField = NULL;
public mixed $keyField = NULL;
/**
* @brief Group by key
*/
public $groupBy = NULL;
public string|array|null $groupBy = NULL;
/**
* @brief The database we will query against
*/
public $db;
public ?IPSDb $db = NULL;
/**
* Constructor
*
* @param string $table Database table
* @param \IPS\Http\Url $baseUrl Base URL
* @param array|null $where WHERE clause
* @param array|null $forceIndex Index to force
* @param \IPS\Db|null $database An instance of \IPS\Db to run the queries against (defaults to current connection)
* @return void
* @param string $table Database table
* @param Url $baseUrl Base URL
* @param array|string|null $where WHERE clause
* @param array|null $forceIndex Index to force
* @param IPSDb|null $database An instance of \IPS\Db to run the queries against (defaults to current connection)
*/
public function __construct( $table, \IPS\Http\Url $baseUrl, $where=NULL, $forceIndex=NULL, $database=NULL )
public function __construct( string $table, Url $baseUrl, array|string $where=null, mixed $forceIndex=null, IPSDb $database=null )
{
$this->table = $table;
$this->where = $where;
$this->index = $forceIndex;
$this->db = $database ?? \IPS\Db::i();
$this->db = $database ?? IPSDb::i();
return parent::__construct( $baseUrl );
}
@@ -96,31 +112,32 @@ class _Db extends Table
* Get rows
* @note This method is called twice, so if there'some expensive operation happening, or if you're calculating something, make sure to cache it.
*
* @param array $advancedSearchValues Values from the advanced search form
* @return array
* @param array|null $advancedSearchValues Values from the advanced search form
* @return array
* @throws Exception
*/
public function getRows( $advancedSearchValues )
public function getRows( array $advancedSearchValues = null ): array
{
/* Specify filter in where clause */
$where = $this->where ? \is_array( $this->where ) ? $this->where : array( $this->where ) : array();
$where = $this->where ? is_array( $this->where ) ? $this->where : array( $this->where ) : array();
if ( $this->filter and isset( $this->filters[ $this->filter ] ) )
{
$where[] = \is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
$where[] = is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
}
/* Add quick search term to where clause if necessary */
if ( $this->quickSearch !== NULL and \IPS\Request::i()->quicksearch )
if ( $this->quickSearch !== NULL and Request::i()->quicksearch )
{
if ( \is_callable( $this->quickSearch ) )
if ( is_callable( $this->quickSearch ) )
{
$quickSearchFunc = $this->quickSearch;
$where[] = $quickSearchFunc( trim( \IPS\Request::i()->quicksearch ) );
$where[] = $quickSearchFunc( trim( Request::i()->quicksearch ) );
}
else
{
$columns = \is_array( $this->quickSearch ) ? $this->quickSearch[0] : $this->quickSearch;
$columns = \is_array( $columns ) ? $columns : array( $columns );
$columns = is_array( $this->quickSearch ) ? $this->quickSearch[0] : $this->quickSearch;
$columns = is_array( $columns ) ? $columns : array( $columns );
$_where = array();
foreach ( $columns as $c )
@@ -128,7 +145,7 @@ class _Db extends Table
$_where[] = "LOWER(`{$c}`) LIKE CONCAT( '%', ?, '%' )";
}
$where[] = array_merge( array( '(' . implode( ' OR ', $_where ) . ')' ), array_fill( 0, \count( $_where ), mb_strtolower( trim( \IPS\Request::i()->quicksearch ) ) ) );
$where[] = array_merge( array( '(' . implode( ' OR ', $_where ) . ')' ), array_fill( 0, count( $_where ), mb_strtolower( trim( Request::i()->quicksearch ) ) ) );
}
}
@@ -137,11 +154,11 @@ class _Db extends Table
{
foreach ( $advancedSearchValues as $k => $v )
{
if ( isset( $this->advancedSearch[ $k ] ) AND $v !== '' AND ( !\is_array( $v ) OR !empty( $v ) ) )
if ( isset( $this->advancedSearch[ $k ] ) AND $v !== '' AND ( !is_array( $v ) OR !empty( $v ) ) )
{
$type = $this->advancedSearch[ $k ];
if ( \is_array( $type ) )
if ( is_array( $type ) )
{
if ( isset( $type[2] ) )
{
@@ -193,7 +210,7 @@ class _Db extends Table
}
break;
case SEARCH_DATE_RANGE:
$timezone = ( \IPS\Member::loggedIn()->timezone ? new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) : NULL );
$timezone = ( Member::loggedIn()->timezone ? new DateTimeZone( Member::loggedIn()->timezone ) : NULL );
if( !$v['start'] AND !$v['end'] )
{
@@ -202,18 +219,18 @@ class _Db extends Table
if ( $v['start'] )
{
if( !( $v['start'] instanceof \IPS\DateTime ) )
if( !( $v['start'] instanceof DateTime ) )
{
$v['start'] = new \IPS\DateTime( $v['start'], $timezone );
$v['start'] = new DateTime( $v['start'], $timezone );
}
$where[] = array( "{$k}>?", $v['start']->getTimestamp() );
}
if ( $v['end'] )
{
if( !( $v['end'] instanceof \IPS\DateTime ) )
if( !( $v['end'] instanceof DateTime ) )
{
$v['end'] = new \IPS\DateTime( $v['end'], $timezone );
$v['end'] = new DateTime( $v['end'], $timezone );
}
$where[] = array( "{$k}<?", $v['end']->getTimestamp() );
@@ -235,7 +252,7 @@ class _Db extends Table
case SEARCH_MEMBER:
if ( $v )
{
$where[] = array( "{$k}=?", ( $v instanceof \IPS\Member ) ? $v->member_id : $v );
$where[] = array( "{$k}=?", ( $v instanceof Member ) ? $v->member_id : $v );
}
else
{
@@ -245,8 +262,8 @@ class _Db extends Table
case SEARCH_NODE:
$nodeClass = $options['class'];
$prop = isset( $options['searchProp'] ) ? $options['searchProp'] : '_id';
if ( !\is_array( $v ) )
$prop = $options['searchProp'] ?? '_id';
if ( !is_array( $v ) )
{
$v = array( $v );
}
@@ -254,7 +271,7 @@ class _Db extends Table
$values = array();
foreach ( $v as $_v )
{
if ( !\is_object( $_v ) )
if ( !is_object( $_v ) )
{
if ( mb_substr( $_v, 0, 2 ) === 's.' )
{
@@ -265,7 +282,7 @@ class _Db extends Table
{
$_v = $nodeClass::load( $_v );
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
continue;
}
@@ -306,7 +323,7 @@ class _Db extends Table
$selects = $this->selects;
$isOtherJoin = false;
if ( \count( $this->joins ) )
if ( count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -315,7 +332,7 @@ class _Db extends Table
$selects[] = $join['select'];
}
if ( isset( $join['type'] ) and \mb_strtolower( $join['type'] ) != 'left' )
if ( isset( $join['type'] ) and mb_strtolower( $join['type'] ) != 'left' )
{
/* Inner join or straight join which will affect the query results */
$isOtherJoin = true;
@@ -325,13 +342,15 @@ class _Db extends Table
/* Count results (for pagination) */
$count = $this->db->select( 'count(*)', $this->table, $where, NULL, NULL, $this->groupBy );
/* Add the joins if we have a where statement and all joins are left for the count */
if ( ( $isOtherJoin or \count( $where ) ) and \count( $this->joins ) )
if ( count( $this->joins ) )
{
foreach( $this->joins as $join )
/* Add the joins if we have a where statement and all joins are left for the count */
if ( ( $isOtherJoin or count( $where ) ) and count( $this->joins ) )
{
$count->join( $join['from'], ( isset( $join['where'] ) ? $join['where'] : null ), ( isset( $join['type'] ) ) ? $join['type'] : 'LEFT' );
foreach ( $this->joins as $join )
{
$count->join( $join['from'], ( $join['where'] ?? null ), ( isset( $join['type'] ) ) ? $join['type'] : 'LEFT' );
}
}
}
@@ -340,13 +359,13 @@ class _Db extends Table
$selectPrefix = ( $this->groupBy ) ? '' : $this->table . '.*, ';
/* Now get column headers */
$query = $this->db->select( ( \count( $selects ) ) ? $selectPrefix . implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ), $this->groupBy );
$query = $this->db->select( ( count( $selects ) ) ? $selectPrefix . implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ), $this->groupBy );
if ( \count( $this->joins ) )
if ( count( $this->joins ) )
{
foreach( $this->joins as $join )
{
$query->join( $join['from'], ( isset( $join['where'] ) ? $join['where'] : null ), ( isset( $join['type'] ) ) ? $join['type'] : 'LEFT' );
$query->join( $join['from'], ( $join['where'] ?? null ), ( isset( $join['type'] ) ) ? $join['type'] : 'LEFT' );
}
}
@@ -354,7 +373,7 @@ class _Db extends Table
{
$results = $query->first();
}
catch( \UnderflowException $e )
catch( UnderflowException $e )
{
$results = array();
}
@@ -393,12 +412,12 @@ class _Db extends Table
}
/* Are we downloading? Bypass Table Limit */
$limit = \IPS\Request::i()->download ? $count : $this->limit;
$limit = Request::i()->download ? $count : $this->limit;
/* Run query */
$rows = array();
$select = $this->db->select(
( \count( $selects ) ) ? $selectPrefix . implode( ', ', $selects ) : '*',
( count( $selects ) ) ? $selectPrefix . implode( ', ', $selects ) : '*',
$this->table,
$where,
$orderBy,
@@ -411,7 +430,7 @@ class _Db extends Table
$select->forceIndex( $this->index );
}
if ( \count( $this->joins ) )
if ( count( $this->joins ) )
{
foreach( $this->joins as $join )
{
@@ -432,14 +451,14 @@ class _Db extends Table
$row = array();
foreach ( $this->include as $k )
{
$row[ $k ] = isset( $_row[ $k ] ) ? $_row[ $k ] : NULL;
$row[ $k ] = $_row[$k] ?? NULL;
}
if( !empty( $advancedSearchValues ) AND !isset( \IPS\Request::i()->noColumn ) )
if( !empty( $advancedSearchValues ) AND !isset( Request::i()->noColumn ) )
{
foreach ( $advancedSearchValues as $k => $v )
{
$row[ $k ] = isset( $_row[ $k ] ) ? $_row[ $k ] : NULL;
$row[ $k ] = $_row[$k] ?? NULL;
}
}
}
@@ -459,7 +478,7 @@ class _Db extends Table
}
/* Are we including this one? */
if( ( ( $this->include !== NULL and !\in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and \in_array( $k, $this->exclude ) ) ) and !array_key_exists( $k, $advancedSearchValues ) )
if( ( ( $this->include !== NULL and !in_array( $k, $this->include ) ) or ( $this->exclude !== NULL and in_array( $k, $this->exclude ) ) ) and !array_key_exists( $k, $advancedSearchValues ) )
{
unset( $row[ $k ] );
continue;
@@ -518,17 +537,17 @@ class _Db extends Table
/**
* User set sortBy is suitable for an SQL sort operation
* @param array $count Result of count(*) query with field names included
* @param array $count Result of count(*) query with field names included
* @return boolean
*/
protected function _isSqlSort( $count )
protected function _isSqlSort( array $count ): bool
{
if ( !$this->sortBy )
{
return false;
}
if( !\is_array( $count ) )
if( !is_array( $count ) )
{
$count = array( $count );
}
@@ -572,7 +591,7 @@ class _Db extends Table
*
* @return array
*/
public function customActions()
public function customActions(): array
{
return array();
}
+159 -134
View File
@@ -11,9 +11,36 @@
namespace IPS\Helpers\Table;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\DateTime;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Checkbox;
use IPS\Helpers\Form\Custom;
use IPS\Helpers\Form\DateRange;
use IPS\Helpers\Form\Member as FormMember;
use IPS\Helpers\Form\Node;
use IPS\Helpers\Form\Radio;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\YesNo;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Member;
use IPS\Node\Model;
use IPS\Output;
use IPS\Request;
use IPS\Session;
use IPS\Theme;
use Throwable;
use function count;
use function defined;
use function intval;
use function is_array;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
@@ -34,7 +61,7 @@ const SEARCH_CHECKBOX = 12;
/**
* List Table Builder
*/
abstract class _Table
abstract class Table
{
/**
* @brief Base URL of the page the list table is on
@@ -44,115 +71,115 @@ abstract class _Table
/**
* @brief Elements to include (defaults to all - can use either this or $exclude)
*/
public $include = NULL;
public ?array $include = NULL;
/**
* @brief Elements to exclude (defaults to none - can use either this or $include)
*/
public $exclude = NULL;
public ?array $exclude = NULL;
/**
* @brief Column to sort results by
*/
public $sortBy;
public ?string $sortBy = NULL;
/**
* @brief Sort Direction - "asc" or "desc"
*/
public $sortDirection = NULL;
public ?string $sortDirection = NULL;
/**
* @brief Columns that are not sortable
*/
public $noSort = array();
public array $noSort = array();
/**
* @brief Default column to sort results by
*/
public $defaultSortBy;
public ?string $defaultSortBy = NULL;
/**
* @brief Default sort direction - "asc" or "desc"
*/
public $defaultSortDirection = NULL;
public ?string $defaultSortDirection = NULL;
/**
* @brief Filters
*/
public $filters = array();
public array $filters = array();
/**
* @brief Current filter
*/
public $filter = NULL;
public mixed $filter = NULL;
/**
* @brief Field to enable quick search on
*/
public $quickSearch = NULL;
public mixed $quickSearch = NULL;
/**
* @brief Whether to show advanced search/sort button or not
* @note It is possible to recreate the form external to the helper (e.g. search) in which case you may not want to show the button
*/
public $showAdvancedSearch = TRUE;
public bool $showAdvancedSearch = TRUE;
/**
* @brief Whether to use the placeholder loading style with this table
*/
public $dummyLoading = FALSE;
public bool $dummyLoading = FALSE;
/**
* @brief Fields to enable advanced sarch on
* @note Keys are the field names. Values are a IPS\Helpers\Table\SEARCH_* constant
*/
public $advancedSearch = array();
public array $advancedSearch = array();
/**
* @brief Number of records to show
*/
public $limit = 25;
public int $limit = 25;
/**
* @brief Number of pages
* @see \IPS\Helpers\Table\getRows
* @see Table::getRows()
*/
public $pages = 1;
public int $pages = 1;
/**
* @brief Current Page
*/
public $page = 1;
public int $page = 1;
/**
* @brief Pagination parameter
*/
protected $paginationKey = 'page';
protected string $paginationKey = 'page';
/**
* @brief Use simple pagination
*/
public $simplePagination = FALSE;
public bool $simplePagination = FALSE;
/**
* @brief Table resort parameter
*/
public $resortKey = 'listResort';
public string $resortKey = 'listResort';
/**
* @brief Language prefix for column names
*/
public $langPrefix = '';
public string $langPrefix = '';
/**
* @brief Language key for table title
*/
public $title = '';
public string $title = '';
/**
* @brief Use realtime features within the table?
*/
public $enableRealtime = false;
public bool $enableRealtime = false;
/**
* @brief Parsers
@@ -167,27 +194,27 @@ abstract class _Table
* @endcode
* @note When implementing, note that this will override the default parser which runs htmlentities, necessary for preventing XSS
*/
public $parsers = array();
public array $parsers = array();
/**
* @brief Column to highlight as the "main" column (e.g. the title)
*/
public $mainColumn = NULL;
public ?string $mainColumn = NULL;
/**
* @brief Additional CSS classes to apply to the table
*/
public $classes = array();
public array $classes = array();
/**
* @brief Additional CSS classes to apply to individual columns
*/
public $rowClasses = array();
public array $rowClasses = array();
/**
* @brief Rows to highlight
*/
public $highlightRows = array();
public array $highlightRows = array();
/**
* @brief Buttons to show on the "root row"
@@ -206,7 +233,7 @@ abstract class _Table
);
* @endcode
*/
public $rootButtons = NULL;
public ?array $rootButtons = NULL;
/**
* @brief Callback function to get buttons for a record
@@ -217,62 +244,62 @@ abstract class _Table
}
* @endcode
*/
public $rowButtons = NULL;
public mixed $rowButtons = NULL;
/**
* @brief Column widths (in percentages)
*/
public $widths = array();
public array $widths = array();
/**
* @brief Template for table
*/
public $tableTemplate;
public array $tableTemplate;
/**
* @brief Template for rows
*/
public $rowsTemplate;
public array $rowsTemplate;
/**
* @brief Sort options (used only on front-end)
*/
public $sortOptions;
public array $sortOptions = array();
/**
* @brief Unique ID for this table
*/
public $uniqueId;
public ?string $uniqueId = NULL;
/**
* @brief Extra HTML to show below filter/search bar
*/
public $extraHtml = '';
public string $extraHtml = '';
/**
* @brief Extra Data
*/
public $extra = NULL;
public mixed $extra = NULL;
/**
* @brief Store advanced search values
*/
protected $advancedSearchValues = NULL;
protected string|array|null $advancedSearchValues = NULL;
/**
* Constructor
*
* @param \IPS\Http\Url $baseUrl Base URL of the page the list table is on
* @param Url $baseUrl Base URL of the page the list table is on
* @return void
*/
public function __construct( \IPS\Http\Url $baseUrl )
public function __construct( Url $baseUrl )
{
/* Set page */
$parameter = $this->paginationKey;
if ( \IPS\Request::i()->$parameter )
if ( Request::i()->$parameter )
{
$this->page = \intval( \IPS\Request::i()->$parameter );
$this->page = intval( Request::i()->$parameter );
if ( !$this->page OR $this->page < 1 )
{
@@ -281,27 +308,27 @@ abstract class _Table
}
/* Set sort options */
if( \IPS\Request::i()->sortby )
if( Request::i()->sortby )
{
$this->sortBy = \IPS\Request::i()->sortby;
$this->sortBy = Request::i()->sortby;
}
if( \IPS\Request::i()->sortdirection )
if( Request::i()->sortdirection )
{
$this->sortDirection = ( mb_strtolower( \IPS\Request::i()->sortdirection ) === 'desc' or mb_strtolower( \IPS\Request::i()->sortdirection ) === 'asc' ) ? mb_strtolower( \IPS\Request::i()->sortdirection ) : NULL;
$this->sortDirection = ( mb_strtolower( Request::i()->sortdirection ) === 'desc' or mb_strtolower( Request::i()->sortdirection ) === 'asc' ) ? mb_strtolower( Request::i()->sortdirection ) : NULL;
}
/* Filter? */
if ( \IPS\Request::i()->filter )
if ( Request::i()->filter )
{
$this->filter = \IPS\Request::i()->filter;
$this->filter = Request::i()->filter;
}
/* Set base URL */
$this->baseUrl = $baseUrl->setQueryString( array( 'filter' => $this->filter, 'sortby' => $this->sortBy, 'sortdirection' => $this->sortDirection ) )->setPage( $this->paginationKey, $this->page );
/* Templates */
$this->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'tables', 'core' ), 'table' );
$this->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'tables', 'core' ), 'rows' );
$this->tableTemplate = array( Theme::i()->getTemplate( 'tables', 'core' ), 'table' );
$this->rowsTemplate = array( Theme::i()->getTemplate( 'tables', 'core' ), 'rows' );
/* Create a unique id used by the template - can be overriden manually if desired */
$this->uniqueId = md5( mt_rand() );
@@ -312,7 +339,7 @@ abstract class _Table
*
* @return string
*/
public function getPaginationKey()
public function getPaginationKey(): string
{
return $this->paginationKey;
}
@@ -320,20 +347,20 @@ abstract class _Table
/**
* Setting the page parameter means we need to recalculate the pages
*
* @param string $property Property we are updating
* @param string $property Property we are updating
* @param mixed $value Value being set
* @return void
*/
public function __set( $property, $value )
public function __set( string $property, mixed $value )
{
if( $property == 'paginationKey' )
{
$this->baseUrl = $this->baseUrl->stripQueryString( $this->paginationKey );
$this->paginationKey = $value;
if ( \IPS\Request::i()->$value )
if ( Request::i()->$value )
{
$this->page = \intval( \IPS\Request::i()->$value );
$this->page = intval( Request::i()->$value );
if ( !$this->page OR $this->page < 1 )
{
$this->page = 1;
@@ -349,9 +376,9 @@ abstract class _Table
* For example, we might allow array( 'last_post', 'author' ) but the $this->sortBy property is often set as "app_table.last_post" meaning
* that the filter drop down doesn't show the selected value correctly.
*
* @return string
* @return string|null
*/
public function getSortByColumn()
public function getSortByColumn(): ?string
{
if ( mb_strpos( $this->sortBy, '.' ) )
{
@@ -367,19 +394,19 @@ abstract class _Table
/**
* Build Advanced Search Form
*
* @return \IPS\Helpers\Form
* @return Form
*/
protected function advancedSearch()
protected function advancedSearch(): Form
{
$form = new \IPS\Helpers\Form( 'advanced_search', 'search', $this->baseUrl, array( 'data-role' => 'advancedSearch' ) );
$form->hiddenValues['filter'] = \IPS\Request::i()->filter;
$form->hiddenValues['sortby'] = \IPS\Request::i()->sortby;
$form->hiddenValues['sortdirection'] = \IPS\Request::i()->sortdirection;
$form = new Form( 'advanced_search', 'search', $this->baseUrl, array( 'data-role' => 'advancedSearch' ) );
$form->hiddenValues['filter'] = Request::i()->filter;
$form->hiddenValues['sortby'] = Request::i()->sortby;
$form->hiddenValues['sortdirection'] = Request::i()->sortdirection;
foreach ( $this->advancedSearch as $k => $type )
{
$options = array();
if ( \is_array( $type ) )
if ( is_array( $type ) )
{
$options = $type[1];
$type = $type[0];
@@ -388,58 +415,58 @@ abstract class _Table
switch ( $type )
{
case SEARCH_CUSTOM:
$form->add( new \IPS\Helpers\Form\Custom( $this->langPrefix . $k, NULL, FALSE, $options ) );
$form->add( new Custom( $this->langPrefix . $k, NULL, FALSE, $options ) );
break;
case SEARCH_CONTAINS_TEXT:
$form->add( new \IPS\Helpers\Form\Text( $this->langPrefix . $k, NULL, FALSE, $options ) );
$form->add( new Text( $this->langPrefix . $k, NULL, FALSE, $options ) );
break;
case SEARCH_QUERY_TEXT:
$form->add( new \IPS\Helpers\Form\Custom( $this->langPrefix . $k, NULL, FALSE, array(
$form->add( new Custom( $this->langPrefix . $k, NULL, FALSE, array(
'getHtml' => function( $element )
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", ( \is_array( $element->value ) AND isset( $element->value[0] ) ) ? $element->value[0] : NULL, $element->required, array(
'c' => \IPS\Member::loggedIn()->language()->addToStack('contains'),
'bw' => \IPS\Member::loggedIn()->language()->addToStack('begins_with'),
'eq' => \IPS\Member::loggedIn()->language()->addToStack('exactly'),
return Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", ( is_array( $element->value ) AND isset( $element->value[0] ) ) ? $element->value[0] : NULL, $element->required, array(
'c' => Member::loggedIn()->language()->addToStack('contains'),
'bw' => Member::loggedIn()->language()->addToStack('begins_with'),
'eq' => Member::loggedIn()->language()->addToStack('exactly'),
) )
. ' '
. \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( "{$element->name}[1]", 'text', ( \is_array( $element->value ) AND isset( $element->value[1] ) ) ? $element->value[1] : NULL, $element->required, NULL, FALSE, NULL, NULL, NULL, '', NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
. Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( "{$element->name}[1]", 'text', ( is_array( $element->value ) AND isset( $element->value[1] ) ) ? $element->value[1] : NULL, $element->required, NULL, FALSE, NULL, NULL, NULL, '', NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
}
) ) );
break;
case SEARCH_DATE_RANGE:
$form->add( new \IPS\Helpers\Form\DateRange( $this->langPrefix . $k, array( 'start' => '', 'end' => '' ), FALSE, $options ) );
$form->add( new DateRange( $this->langPrefix . $k, array( 'start' => '', 'end' => '' ), FALSE, $options ) );
break;
case SEARCH_SELECT:
$form->add( new \IPS\Helpers\Form\Select( $this->langPrefix . $k, NULL, FALSE, $options ) );
$form->add( new Select( $this->langPrefix . $k, NULL, FALSE, $options ) );
break;
case SEARCH_RADIO:
$form->add( new \IPS\Helpers\Form\Radio( $this->langPrefix . $k, NULL, FALSE, $options ) );
$form->add( new Radio( $this->langPrefix . $k, NULL, FALSE, $options ) );
break;
case SEARCH_MEMBER:
$form->add( new \IPS\Helpers\Form\Member( $this->langPrefix . $k, NULL, FALSE, $options ) );
$form->add( new FormMember( $this->langPrefix . $k, NULL, FALSE, $options ) );
break;
case SEARCH_NODE:
$form->add( new \IPS\Helpers\Form\Node( $this->langPrefix . $k, 0, FALSE, $options ) );
$form->add( new Node( $this->langPrefix . $k, 0, FALSE, $options ) );
break;
case SEARCH_NUMERIC:
case SEARCH_NUMERIC_TEXT:
$form->add( new \IPS\Helpers\Form\Custom( $this->langPrefix . $k, NULL, FALSE, array(
$form->add( new Custom( $this->langPrefix . $k, NULL, FALSE, array(
'getHtml' => function( $element )
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", ( \is_array( $element->value ) AND isset( $element->value[0] ) ) ? $element->value[0] : NULL, $element->required, array(
'any' => \IPS\Member::loggedIn()->language()->addToStack('any'),
'gt' => \IPS\Member::loggedIn()->language()->addToStack('gt'),
'lt' => \IPS\Member::loggedIn()->language()->addToStack('lt'),
'eq' => \IPS\Member::loggedIn()->language()->addToStack('exactly'),
return Theme::i()->getTemplate( 'forms', 'core' )->select( "{$element->name}[0]", ( is_array( $element->value ) AND isset( $element->value[0] ) ) ? $element->value[0] : NULL, $element->required, array(
'any' => Member::loggedIn()->language()->addToStack('any'),
'gt' => Member::loggedIn()->language()->addToStack('gt'),
'lt' => Member::loggedIn()->language()->addToStack('lt'),
'eq' => Member::loggedIn()->language()->addToStack('exactly'),
),
FALSE,
NULL,
@@ -451,16 +478,16 @@ abstract class _Table
'eq' => array( $element->name . '-qty' ),
) )
. ' '
. \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->number( "{$element->name}[1]", ( \is_array( $element->value ) AND isset( $element->value[1] ) ) ? $element->value[1] : NULL, $element->required, NULL, FALSE, NULL, NULL, NULL, 0, NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
. Theme::i()->getTemplate( 'forms', 'core', 'global' )->number( "{$element->name}[1]", ( is_array( $element->value ) AND isset( $element->value[1] ) ) ? $element->value[1] : NULL, $element->required, NULL, FALSE, NULL, NULL, NULL, 0, NULL, FALSE, NULL, array(), array(), array( $element->name . '-qty' ) );
}
) ) );
break;
case SEARCH_CHECKBOX:
$form->add( new \IPS\Helpers\Form\Checkbox( $this->langPrefix . $k, NULL, FALSE, $options ) );
$form->add( new Checkbox( $this->langPrefix . $k, NULL, FALSE, $options ) );
break;
case SEARCH_BOOL:
$form->add( new \IPS\Helpers\Form\YesNo( $this->langPrefix . $k, TRUE, FALSE, $options ) );
$form->add( new YesNo( $this->langPrefix . $k, TRUE, FALSE, $options ) );
break;
case HEADER:
$form->addHeader( $this->langPrefix . $k );
@@ -486,10 +513,10 @@ abstract class _Table
if ( !empty( $this->advancedSearch ) )
{
/* Are we displaying the advanced search form? */
if ( \IPS\Request::i()->advancedSearchForm )
if ( Request::i()->advancedSearchForm )
{
/* If we are showing just the advanced search form, send a noindex meta tag */
\IPS\Output::i()->metaTags['robots'] = 'noindex';
Output::i()->metaTags['robots'] = 'noindex';
return (string) $this->advancedSearch();
}
@@ -508,39 +535,39 @@ abstract class _Table
{
if ( $this->pages and $this->page > $this->pages )
{
\IPS\Output::i()->redirect( $this->baseUrl->setPage( $this->paginationKey, 1 ), NULL, 303 );
Output::i()->redirect( $this->baseUrl->setPage( $this->paginationKey, 1 ), NULL, 303 );
}
}
/* Add link tags */
if ( $this->page != 1 )
{
if( !isset( \IPS\Output::i()->linkTags['canonical'] ) )
if( !isset( Output::i()->linkTags['canonical'] ) )
{
\IPS\Output::i()->linkTags['canonical'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page );
Output::i()->linkTags['canonical'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page );
}
\IPS\Output::i()->linkTags['first'] = (string) $this->baseUrl->setPage( $this->paginationKey, 1 );
\IPS\Output::i()->linkTags['prev'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page - 1 );
Output::i()->linkTags['first'] = (string) $this->baseUrl->setPage( $this->paginationKey, 1 );
Output::i()->linkTags['prev'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page - 1 );
}
/* If we literally requested ?page=1 add canonical tag to get rid of the page query string param */
elseif( isset( $this->baseUrl->data[ \IPS\Http\Url::COMPONENT_QUERY ][ $this->paginationKey ] ) )
elseif( isset( $this->baseUrl->data[ Url::COMPONENT_QUERY ][ $this->paginationKey ] ) )
{
\IPS\Output::i()->linkTags['canonical'] = (string) $this->baseUrl->setPage();
Output::i()->linkTags['canonical'] = (string) $this->baseUrl->setPage();
}
if ( $this->pages > $this->page )
{
\IPS\Output::i()->linkTags['next'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page + 1 );
Output::i()->linkTags['next'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->page + 1 );
}
if ( $this->pages != $this->page )
{
\IPS\Output::i()->linkTags['last'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->pages );
Output::i()->linkTags['last'] = (string) $this->baseUrl->setPage( $this->paginationKey, $this->pages );
}
/* No rows to show? Add a noindex but follow for crawling later to check if content exists */
if ( ! \count( $rows ) )
if ( ! count( $rows ) )
{
\IPS\Output::i()->metaTags['robots'] = 'noindex, follow';
Output::i()->metaTags['robots'] = 'noindex, follow';
}
/* Get table headers */
@@ -548,19 +575,19 @@ abstract class _Table
/* If this is an AJAX request, just return them, with pagination */
$resortKey = $this->resortKey;
if( \IPS\Request::i()->isAjax() and \IPS\Request::i()->$resortKey )
if( Request::i()->isAjax() and Request::i()->$resortKey )
{
if ( \count( $rows ) )
if ( count( $rows ) )
{
$rowsTemplate = $this->rowsTemplate;
$rowHtml = $rowsTemplate( $this, $headers, $rows, $this->mainColumn, $this->rootButtons, array() );
}
else
{
$rowHtml = \IPS\Theme::i()->getTemplate( 'tables', 'core', 'front' )->noRows();
$rowHtml = Theme::i()->getTemplate( 'tables', 'core', 'front' )->noRows();
}
\IPS\Output::i()->json( array( 'rows' => $rowHtml, 'pagination' => \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->pagination( $this->baseUrl, $this->pages, $this->page, $this->limit, TRUE, $this->paginationKey, $this->simplePagination ), 'extraHtml' => $this->extraHtml ) );
Output::i()->json( array( 'rows' => $rowHtml, 'pagination' => Theme::i()->getTemplate( 'global', 'core', 'global' )->pagination( $this->baseUrl, $this->pages, $this->page, $this->limit, TRUE, $this->paginationKey, $this->simplePagination ), 'extraHtml' => $this->extraHtml ) );
}
/* Otherwise, show the full table */
else
@@ -572,47 +599,45 @@ abstract class _Table
}
/* Add JS */
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_core.js', 'core', 'global' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'global_core.js', 'core', 'global' ) );
/* Build table */
$tableTemplate = $this->tableTemplate;
return $tableTemplate( $this, $headers, $rows, ( \is_array( $this->quickSearch ) ? $this->quickSearch[1] : $this->quickSearch ), !empty( $this->advancedSearch ) );
return $tableTemplate( $this, $headers, $rows, ( is_array( $this->quickSearch ) ? $this->quickSearch[1] : $this->quickSearch ), !empty( $this->advancedSearch ) );
}
}
catch ( \Exception $e )
catch ( Exception | Throwable $e )
{
\IPS\IPS::exceptionHandler( $e );
}
catch ( \Throwable $e )
{
\IPS\IPS::exceptionHandler( $e );
IPS::exceptionHandler( $e );
}
return '';
}
/**
* Convert the value of a search field into something we can put in the query string
*
* @param array $values The values
* @param array $values The values
* @return array
*/
protected function _convertSearchValuesForQueryString( $values )
protected function _convertSearchValuesForQueryString( array $values ): array
{
$return = array();
foreach( $values as $k => $v )
{
if ( \is_array( $v ) )
if ( is_array( $v ) )
{
$return[ $k ] = $this->_convertSearchValuesForQueryString( $v );
}
elseif ( $v instanceof \IPS\DateTime )
elseif ( $v instanceof DateTime )
{
$return[ $k ] = $v->getTimestamp();
}
elseif ( $v instanceof \IPS\Member )
elseif ( $v instanceof Member )
{
$return[ $k ] = $v->name;
}
elseif ( $v instanceof \IPS\Node\Model )
elseif ( $v instanceof Model )
{
if ( isset( $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ] ) AND
!( $v instanceof $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ][1]['class'] ) )
@@ -624,7 +649,7 @@ abstract class _Table
$return[ $k ] = $v->_id;
}
}
else if( isset( $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ][0] ) and $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ][0] === \IPS\Helpers\Table\SEARCH_BOOL )
else if( isset( $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ][0] ) and $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ][0] === SEARCH_BOOL)
{
$return[ $k . '_checkbox' ] = $v;
}
@@ -640,10 +665,10 @@ abstract class _Table
/**
* Get rows
*
* @param array $advancedSearchValues Values from the advanced search form
* @param array|null $advancedSearchValues Values from the advanced search form
* @return array
*/
abstract public function getRows( $advancedSearchValues );
abstract public function getRows( array $advancedSearchValues = NULL ): array;
/**
* Return the table headers
@@ -651,7 +676,7 @@ abstract class _Table
* @param array|NULL $advancedSearchValues Advanced search values
* @return array
*/
public function getHeaders( $advancedSearchValues )
public function getHeaders( array $advancedSearchValues=NULL ): array
{
/* Get headers */
if ( empty( $this->include ) )
@@ -668,7 +693,7 @@ abstract class _Table
}
else
{
if( !empty( $advancedSearchValues ) AND !isset( \IPS\Request::i()->noColumn ) )
if( !empty( $advancedSearchValues ) AND !isset( Request::i()->noColumn ) )
{
$headers = array_combine( $this->include, $this->include );
@@ -706,17 +731,17 @@ abstract class _Table
* @param string|null $action Specific action to check (hide/unhide, etc.) or NULL for a generic check
* @return bool
*/
public function canModerate( $action=NULL )
public function canModerate( string $action=NULL ): bool
{
return FALSE;
}
/**
* Get the advanced search values
*
* @return array
* @return array|string|null
*/
public function getAdvancedSearchValues()
public function getAdvancedSearchValues(): array|string|null
{
/* Advanced Search */
$advancedSearchValues = array();
@@ -734,7 +759,7 @@ abstract class _Table
if ( !empty( $this->advancedSearch ) )
{
/* Are we displaying the advanced search form? */
if ( \IPS\Request::i()->advancedSearchForm )
if ( Request::i()->advancedSearchForm )
{
return (string) $this->advancedSearch();
}
@@ -762,7 +787,7 @@ abstract class _Table
if ( !empty( $this->advancedSearchValues ) )
{
$this->baseUrl = $this->baseUrl->setQueryString( array_merge( array( 'advanced_search_submitted' => 1, 'csrfKey' => \IPS\Session::i()->csrfKey ), $this->_convertSearchValuesForQueryString( $advancedSearchValuesQuery ) ) );
$this->baseUrl = $this->baseUrl->setQueryString( array_merge( array( 'advanced_search_submitted' => 1, 'csrfKey' => Session::i()->csrfKey ), $this->_convertSearchValuesForQueryString( $advancedSearchValuesQuery ) ) );
}
}
}
@@ -775,10 +800,10 @@ abstract class _Table
* Return the sort direction to use for links
*
* @note Abstracted so other table helper instances can adjust as needed
* @param string $column Sort by string
* @return string [asc|desc]
* @param string $column Sort by string
* @return string|null [asc|desc]
*/
public function getSortDirection( $column )
public function getSortDirection( string $column ): ?string
{
/* If the column we are sorting by is the default sort, use the default sort direction */
if ( $this->defaultSortBy AND $column == $this->defaultSortBy AND $this->defaultSortDirection )