<?php defined('SYSPATH') or die('No direct access allowed.'); /** * This class is for rendering a table of data. * * @package lnApp * @category Helpers * @author Deon George * @copyright (c) 2009-2013 Deon George * @license http://dev.leenooks.net/license.html */ abstract class lnApp_Table { private $data = NULL; private $columns = array(); // Our columns that we need to display private $filters = array(); // Our datafomrating that we need to use private $filters_added = FALSE; private $jssort = FALSE; // Use the JS pager and sorter private $other = ''; // If we are only listing x items, this will be the other summary line private $other_col = NULL; // If we are only listing x items, this will be the other summary line private $po = NULL; // If we are paging the results, this is the page we are displaying private $page = FALSE; // If we should page the results private $page_rows = 0; // Number of rows per page private $prepend = array(); // Our data formating that we need to use, prepended to the value private $postproc = array(); // Our data formating that we need to use after the value is determined private $select = array(); // Our select form details public function __toString() { try { return (string)$this->render(); } catch (Exception $e) { return $e->getMessage(); } } public function columns($cols) { $this->columns = $cols; return $this; } public function data($data) { $this->data = $data; return $this; } private function evaluate($d,$key) { if (count($this->filters) AND ! $this->filters_added) { $d->display_filters($this->filters); // So we dont keep extending our display_filters $this->filters_added = TRUE; } if (is_array($d) AND isset($d[$key])) $x = $d[$key]; // If the key is a method, we need to eval it elseif (preg_match('/\(/',$key) OR preg_match('/-\>/',$key)) eval("\$x = \$d->$key;"); elseif (preg_match('/^__VALUE__$/',$key)) $x = $d; else $x = isset($d->{$key}) ? $d->display($key) : ''; // We cant display array values if (is_array($x)) $x = array_shift($x); if (isset($this->prepend[$key]) AND $x) { foreach ($this->prepend[$key] as $act => $data) { switch ($act) { case 'checkbox': $x = sprintf('<label class="checkbox">%s %s</label>',Form::checkbox($data,$x,FALSE,array('nocg'=>TRUE)),$x); // @todo Check those not yet checked break; case 'input': if (preg_match('/__VALUE__/',$data['key'])) $data['key'] = preg_replace('/__VALUE__/',$x,$data['key']); $x = Form::input($data['key'],isset($data['values'][$x]) ? $data['values'][$x] : '',array('placeholder'=>$x,'label'=>$x)); break; case 'url': $x = HTML::anchor($data.$x,$x); break; case 'url_resolve': $filtermatchall = array(); preg_match_all('/%(\w+)(\|.+)?(\/[lUC])?%/U',$data,$filtermatchall); foreach ($filtermatchall[1] as $id=>$key) $data = str_replace($filtermatchall[0][$id],$d->display($key),$data); $x = HTML::anchor($data,$x); break; default: throw HTTP_Exception::factory(501,'Unknown action :action',array(':action'=>$act)); } } } if (isset($this->postproc[$key]) and $x) { foreach ($this->postproc[$key] as $act => $data) { switch ($act) { case 'yesno': $x = StaticList_YesNo::get($x,TRUE); break; case 'trim': if (strlen($x) > $data) { $x = sprintf('<abbr title="%s">%s</abbr>',$x,substr($x,0,$data-3).str_repeat('.',3)); } break; default: throw HTTP_Exception::factory(501,'Unknown action :action',array(':action'=>$act)); } } } return $x; } public static function factory() { return new Table; } /** * Apply additional display filters to the results */ public function filters($cols) { $this->filters = $cols; return $this; } public function jssort($table) { $this->jssort = $table; return $this; } public function page_items($num) { $this->page = TRUE; $this->page_rows = $num; return $this; } public function prepend($cols) { $this->prepend = $cols; return $this; } public function postproc($cols) { $this->postproc = $cols; return $this; } private function process() { $result = array(); $row = 0; foreach ($this->data as $o) { $row++; $c = 0; // So our filters are added for this record. $this->filters_added = FALSE; // If we are HTML paging if ($this->page) { if ($row < $this->po->current_first_item()) continue; elseif ($row > $this->po->current_last_item()) break; } // If we are just listing page_rows items and a summary line as a result of exceeding that if ($this->other_col AND $row>$this->page_rows) { $this->other += $this->evaluate($o,$this->other_col); // Otherwise rendering our rows } else { foreach ($this->columns as $col => $label) { $c++; $result[$row]['val'][$c] = $this->evaluate($o,$col); } } } return $result; } private function render() { $output = ''; // If we need to page the results if ($this->page) { $this->po = new Pagination(array( 'total_items'=>count($this->data), 'items_per_page'=>$this->page_rows, )); $output .= (string)$this->po; } if ($this->select) $output .= Form::open($this->select['form']); $output .= View::factory('table') ->set('jssort',($this->jssort AND ! $this->page) ? $this->jssort : FALSE) ->set('other',$this->other) ->set('th',$this->columns) ->set('td',$this->process()); // Use the javascript paging if ($this->jssort AND ! $this->page) { Script::factory() ->type('stdin') ->data(' $(document).ready(function() { $.extend($.tablesorter.themes.bootstrap, { // these classes are added to the table. To see other table classes available, // look here: http://twitter.github.com/bootstrap/base-css.html#tables table : "table table-striped", header : "bootstrap-header", // give the header a gradient background footerRow : "", footerCells: "", icons : "", // add "icon-white" to make them white; this icon class is added to the <i> in the header sortNone : "bootstrap-icon-unsorted", sortAsc : "icon-chevron-up", sortDesc : "icon-chevron-down", active : "", // applied when column is sorted hover : "", // use custom css here - bootstrap class may not override it filterRow : "", // filter row class even : "", // odd row zebra striping odd : "" // even row zebra striping }); $("#list-table'.($this->jssort ? '-'.$this->jssort : '').'").tablesorter({ theme: "bootstrap", widthFixed : true, headerTemplate : "{content} {icon}", // Add icon for jui theme; new in v2.7! widgets: [ "uitheme", "stickyHeaders", "filter" ], widgetOptions : { stickyHeaders_offset: ".navbar-fixed-top", } }).tablesorterPager({ // target the pager markup - see the HTML block below container : $(".pager'.($this->jssort ? '-'.$this->jssort : '').'"), output : "{startRow} - {endRow} / {filteredRows} ({totalRows})", fixedHeight: true, removeRows : false, cssGoto : ".gotoPage" }); }); '); Script::factory() ->type('file') ->data('media/vendor/mottie-tablesorter/js/jquery.tablesorter.min.js'); Script::factory() ->type('file') ->data('media/vendor/mottie-tablesorter/js/jquery.tablesorter.widgets.min.js'); Script::factory() ->type('file') ->data('media/vendor/mottie-tablesorter/js/jquery.tablesorter.pager.min.js'); Style::factory() ->type('file') ->data('media/vendor/mottie-tablesorter/css/theme.bootstrap.css'); Style::factory() ->type('file') ->data('media/vendor/mottie-tablesorter/css/jquery.tablesorter.pager.css'); // @todo The theme should be moved from here, but Controller_Media will need to know about admin requests for media/ Style::factory() ->type('file') ->data('media/theme/baseadmin/css/custom/tablesort.css'); } if ($this->select) { $output .= Form::button('submit','Submit',array('class'=>'btn btn-primary','value'=>$this->select['submit'])); foreach ($this->select['hidden'] as $k=>$v) $output .= Form::hidden($k,$v); $output .= Form::close(); } return $output; } public function select($form,$submit='',array $hidden=array()) { $this->select['form'] = $form; $this->select['submit'] = $submit; $this->select['hidden'] = $hidden; return $this; } // @deprecated public static function display($data,$rows,array $cols,array $option) { $prepend = $headers = array(); foreach ($cols as $k=>$v) { if (isset($v['label'])) $headers[$k] = $v['label']; if (isset($v['url'])) $prepend[$k] = array('url'=>$v['url']); } return self::factory() ->data($data) ->jssort(TRUE) ->page_items($rows) ->columns($headers) ->prepend($prepend); /* // This JS is failing, any pressing of select all, unselect all, toggle is propaging up and submitting the form Script::factory() ->type('stdin') ->data(' (function($) { // Enable Range Selection $.fn.enableCheckboxRangeSelection = function() { var lastCheckbox = null; var followOn = 0; var $spec = this; $spec.bind("click", function(e) { if (lastCheckbox != null && e.shiftKey) { x = y = 0; if (followOn != 0) { if ($spec.index(lastCheckbox) < $spec.index(e.target)) { x = 1 - ((followOn == 1) ? 1 : 0); } else { y = 1 - ((followOn == -1) ? 1 : 0); } } $spec.slice( Math.min($spec.index(lastCheckbox) - x, $spec.index(e.target)) + 1, Math.max($spec.index(lastCheckbox), $spec.index(e.target)) + y ).attr("checked",function() { return ! this.checked; }) .parent().parent().toggleClass("selected"); followOn = ($spec.index(lastCheckbox) < $spec.index(e.target)) ? 1 : -1; } else { followOn = 0; } lastCheckbox = e.target; }); return $spec; }; // Enable Toggle, (De)Select All $.fn.check = function(mode) { // if mode is undefined, use "on" as default var mode = mode || "on"; switch(mode) { case "on": $("#select-table tr:not(.head)") .filter(":has(:checkbox:not(checked))") .toggleClass("selected") break; case "off": $("#select-table tr:not(.head)") .filter(":has(:checkbox:checked)") .toggleClass("selected") break; case "toggle": $("#select-table tr:not(.head)") .toggleClass("selected"); break; } return this.each(function(e) { switch(mode) { case "on": this.checked = true; break; case "off": this.checked = false; break; case "toggle": this.checked = !this.checked; break; } }); }; })(jQuery); // Bind our actions $(document).ready(function() { $("#select-table :checkbox").enableCheckboxRangeSelection(); $("#select-menu > #toggle").bind("click",function() { $("#select-table :checkbox").check("toggle"); }); $("#select-menu > #all_on").bind("click",function() { $("#select-table :checkbox").check("on"); }); $("#select-menu > #all_off").bind("click",function() { $("#select-table :checkbox").check("off"); }); // Click to select Row $("#select-table tr:not(.head)") .filter(":has(:checkbox:checked)") .addClass("selected") .end() .click(function(e) { $(this).toggleClass("selected"); if (e.target.type !== "checkbox") { $(":checkbox", this).attr("checked", function() { return !this.checked; }); } }); // Double click to select a row $("#select-table tr:not(.head)") .dblclick(function(e) { window.location = $("a", this).attr("href"); }); }); '); */ } // This enables us to page through many selected items // @todo This is currently not usable, since our JS above needs to be fixed to work with tablesorter public static function page($key) { // We have preference for parameters passed to the action. if (is_null($id = Request::current()->param('id'))) { // First save our POST id data into the session, so we dont need it when going to each page if (isset($_POST['id']) AND is_array($_POST['id'])) Session::instance()->set('page_table_view'.$key,'id'); if ($ids = Session::instance()->get('page_table_view'.$key)) { $pag = new Pagination(array( 'total_items'=>count($ids), 'items_per_page'=>1, )); return array($ids[$pag->current_first_item()-1],(string)$pag); } } // If we get here, then there is no previous data to retrieve. return array($id,''); } } ?>