src/ui/components/datatables/src/Html/Builder.php
<?php
/**
* Part of the Antares package.
*
* NOTICE OF LICENSE
*
* Licensed under the 3-clause BSD License.
*
* This source file is subject to the 3-clause BSD License that is
* bundled with this package in the LICENSE file.
*
* @package Antares Core
* @version 0.9.0
* @author Antares Team
* @license BSD License (3-clause)
* @copyright (c) 2017, Antares
* @link http://antaresproject.io
*/
namespace Antares\Datatables\Html;
use Antares\Datatables\Contracts\DatatabledContract;
use Antares\Datatables\Adapter\ColumnFilterAdapter;
use Antares\Datatables\Adapter\GroupsFilterAdapter;
use Yajra\Datatables\Html\Builder as BaseBuilder;
use Antares\Datatables\Adapter\FilterAdapter;
use Antares\Datatables\Adapter\OrderAdapter;
use Illuminate\Contracts\Config\Repository;
use Antares\Asset\JavaScriptExpression;
use Antares\Asset\JavaScriptDecorator;
use Illuminate\Contracts\View\Factory;
use Antares\Html\Support\FormBuilder;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Collection;
use Illuminate\Http\JsonResponse;
use Illuminate\Events\Dispatcher;
use Yajra\Datatables\Html\Column;
use Antares\Support\Facades\Form;
use Antares\Support\Expression;
use Illuminate\Routing\Router;
use Antares\Html\HtmlBuilder;
class Builder extends BaseBuilder
{
/**
* defered data
*
* @var array
*/
protected $deferredData = [];
/**
* whether use table global search
*
* @var boolean
*/
protected $searchable = true;
/**
* whether use table mass actions
*
* @var boolean
*/
protected $massable = true;
/**
* whether table is groupable
*
* @var boolean
*/
protected $groupable = false;
/**
* mass actions for datatable instance
*
* @var array
*/
protected $massActions = [];
/**
* datatable setter
*
* @var \Antares\Datatables\Services\DataTable
*/
protected $datatable;
/**
* builder query
*
* @var mixed
*/
protected $query;
/**
* Column filter adapter instance
*
* @var ColumnFilterAdapter
*/
protected $columnFilterAdapter;
/**
* Datatable behaviors container
*
* @var array
*/
protected $behaviors = [
'column_filters' => false
];
/**
* Table attributes container
*
* @var array
*/
protected $tableAttributes = [
'class' => 'table dataTable billevo-table',
'id' => 'dataTableBuilder',
'cellspacing' => '0',
'width' => '100%'
];
/**
* Table container attributes
*
* @var array
*/
protected $containerAttributes = [
'class' => 'tbl-c',
];
/** @var array * */
protected $attributes = [
"bFilter" => true,
'iDisplayLength' => 10,
'bLengthChange' => true,
'bInfo' => false,
"columnDefs" => [
],
"serverSide" => true,
"dom" => '<"dt-area-top"i>rt<"dt-area-bottom pagination pagination--type2" fpL><"clear">',
"responsive" => true,
"bProcessing" => false,
"processing" => false,
"oLanguage" => [
"oPaginate" => [
"sPrevious" => "<i class='zmdi zmdi-long-arrow-left dt-pag-left'></i>",
"sNext" => "<i class='zmdi zmdi-long-arrow-right dt-pag-right'></i>",
],
"sLengthMenu" => "_MENU_",
],
'lengthMenu' => [
[10, 25, 50],
[10, 25, 50]
],
];
/**
* filter adapter instance
*
* @var \Antares\Datatables\Adapter\FilterAdapter
*/
protected $filterAdapter;
/**
* datatable instance name
*
* @var String
*/
protected $name;
/**
* datatables fetch method
*
* @var String
*/
protected $method = 'POST';
/**
*
* @var Dispatcher
*/
protected $dispatcher;
/**
*
* @var Router
*/
protected $router;
/**
* List of additional table selects
*
* @var array
*/
protected $selects = [];
/**
* Right ctrls container
*
* @var String
*/
protected $rightCtrls = null;
/**
* Whether scripts are disabled
*
* @var Boolean
*/
protected $disabledScripts = false;
/**
* Constructing
*
* @param Repository $config
* @param Factory $view
* @param HtmlBuilder $html
* @param UrlGenerator $url
* @param FormBuilder $form
*/
public function __construct(Repository $config, Factory $view, HtmlBuilder $html, UrlGenerator $url, FormBuilder $form, FilterAdapter $filterAdapter, Router $router, Dispatcher $dispatcher)
{
$this->config = $config;
$this->view = $view;
$this->html = $html;
$this->url = $url;
$this->collection = new Collection;
$this->form = $form;
$this->filterAdapter = $filterAdapter;
$this->router = $router;
$this->dispatcher = $dispatcher;
$this->tableAttributes['id'] = $this->tableAttributes['id'] . str_random(3);
}
protected function getGlobalSearchValue()
{
if (request()->has('search') && !is_array($value = request()->get('search'))) {
return $value;
}
return false;
}
/**
* Get generated raw scripts.
*
* @return string
*/
public function generateScripts()
{
app('antares.asset')->container('antares/foundation::application')
->add('gridstack', '/webpack/view_datatables.js', ['webpack_gridstack', 'app_cache'])
->add('webpack_forms_basic', '/webpack/forms_basic.js', ['app_cache']);
$columns = $this->hasColumnFilter() ? $this->getColumnFilterAdapter()->getColumns()->toArray() : $this->collection->toArray();
array_set($this->attributes, 'iDisplayLength', $this->datatable->getPerPage());
$args = array_merge(
$this->attributes, ['ajax' => $this->ajax, 'columns' => $columns]
);
$searching = (($value = $this->getGlobalSearchValue()) !== false) ? "data.search.value=instance.find('.mdl-textfield__input').val();" : '';
$groupsFilter = app(\Antares\Datatables\Adapter\GroupsFilterAdapter::class);
$sessionValue = $groupsFilter->getSessionValue();
$cols = '';
if ($sessionValue && !ajax()) {
$columns = [];
foreach ($this->collection as $columnIndex => $column) {
$value = '';
$regex = false;
$fromSession = array_get($sessionValue, $columnIndex, $sessionValue);
if ($column->data == array_get($fromSession, 'data') && $column->name == array_get($fromSession, 'name')) {
$value = array_get($fromSession, 'search.value');
$regex = true;
}
$columns[] = [
'data' => $column->data,
'name' => $column->name,
'searchable' => $column->searchable,
'orderable' => $column->orderable,
'search' => [
'value' => $value,
'regex' => $regex
],
];
}
$cols = 'data.columns = ' . JavaScriptDecorator::decorate($columns) . ';';
}
$eventAfterSearch = (request()->has('search') && !request()->ajax()) ? '$(document).trigger( "datatables.searchLoaded", [ dtInstance,data,' . count(config('search.datatables')) . ' ] );' : '';
$ajax = <<<EOD
function (data, callback, settings) {
if(data.draw===1){
$cols
}
var dtInstance=$(settings.oInstance);
var instance = dtInstance.closest('.grid-stack-item-content').length>0?dtInstance.closest('.grid-stack-item-content'):dtInstance.closest('.tbl-c');
$searching
if (instance.length > 0) {
instance.LoadingOverlay('show');
}
settings.jqXHR = $.ajax({
"dataType": 'json',
"timeout": 20000,
"type": "%s",
"url": "%s",
"data": data,
"success": callback
}).always(function (data) {
$eventAfterSearch
if (instance.length > 0) {
instance.LoadingOverlay('hide');
}
$(document).trigger( "datatablesLoaded", [ dtInstance ] );
});
}
EOD;
$url = $this->getTargetUrl();
$args['ajax'] = new JavaScriptExpression(sprintf($ajax, $this->method, $url));
$id = $this->tableAttributes['id'];
if (isset($args['fnRowCallback'])) {
$args['fnRowCallback'] = new JavaScriptExpression($args['fnRowCallback']);
}
$args['fnRowCallback'] = new JavaScriptExpression("function( nRow, aData, iDisplayIndex, iDisplayIndexFull ){
if($(nRow).find('.dt-actions').length<=0){
$(nRow).addClass('no-actions');
}
}");
$args['fnDrawCallback'] = new JavaScriptExpression("function (oSettings) {
var tblC=this.closest('.tbl-c');
if ( this.fnGetData().length === 0 ) {
tblC.addClass('tbl-c--zd');
}else{
tblC.removeClass('tbl-c--zd');
}
}");
$args['initComplete'] = new JavaScriptExpression(isset($args['initComplete']) ? $args['initComplete'] : 'function () {}');
$args['columnDefs'][] = new JavaScriptExpression('{ responsivePriority:0, targets: -1 }');
$parameters = JavaScriptDecorator::decorate($args);
$variables = JavaScriptDecorator::decorate([
'table' => new JavaScriptExpression('$("#' . $id . '")'),
'tr' => new JavaScriptExpression("$('#" . $id . " tr')")
]);
$javascript = file_get_contents(sandbox_path('packages/core/js/datatables.js'));
$instanceName = 'dataTable' . str_random(3);
$oTable = 'oTable' . str_random(3);
$javascript = str_replace('inject.id', "'#$id'", $javascript);
$javascript = str_replace('instance', $instanceName, $javascript);
$javascript = str_replace('oTable', $oTable, $javascript);
$javascript = str_replace('inject.variables', $variables, $javascript);
$javascript = str_replace('inject.options', $parameters, $javascript);
$javascript = str_replace('inject.contextMenu.build', $parameters, $javascript);
return $javascript;
}
protected function getTargetUrl()
{
if ($this->ajax) {
return $this->ajax;
}
$action = $this->router->getCurrentRoute()->getAction();
list($controller, $method) = explode('@', $action['controller']);
if (is_subclass_of($controller, DatatabledContract::class)) {
return $this->url->action('\\' . $controller . '@datatable');
}
return $this->url->current();
}
/**
* groupable setter
*
* @param boolean $groupable
* @return \Antares\Datatables\Html\Builder
*/
public function groupable($groupable)
{
$this->groupable = $groupable;
return $this;
}
/**
* global search setter
*
* @param boolean $searchable
* @return Builder
*/
public function searchable($searchable)
{
$this->searchable = $searchable;
return $this;
}
/**
* mass actions setter
*
* @param boolean $massable
* @return Builder
*/
public function massable($massable)
{
$this->massable = $massable;
return $this;
}
/**
* Generate DataTable's table html.
*
* @param array $attributes
* @param boolean $drawFooter
* @return string
*/
public function table(array $attributes = [], $drawFooter = false)
{
if (count($this->deferredData)) {
return $this->tableDeferred($attributes);
}
$this->tableAttributes = $attributes ?: $this->tableAttributes;
$scrollable = '';
if ($this->datatable->count() > 10 or strlen($this->ajax) > 0) {
$scrollable = 'data-scrollable';
}
return $this->tableInit() . $this->beforeTable() . '<table ' . $scrollable . ' data-table-init = "true" ' . $this->html->attributes($this->tableAttributes) . '></table>' . $this->afterTable();
}
/**
* triggers before table
*
* @return string
*/
protected function beforeTable()
{
$hasMassActions = $this->hasMassActions();
$filters = $this->filterAdapter->getFilters();
$hasColumnFilter = $this->hasColumnFilter();
if (!$hasMassActions and ! $this->searchable and empty($this->selects) and ! $filters and ! $hasColumnFilter) {
return '';
}
$return = '<div class="card-ctrls mt24"><div class="card-ctrls__left">';
if (!empty($this->selects)) {
$return .= implode('', $this->selects);
}
if ($this->searchable) {
$value = (($value = $this->getGlobalSearchValue()) !== false) ? $value : '';
$return .= '<div class="search-box search-box--dark search-box--big ' . (!empty($this->selects) ? 'ml25' : '') . ' mr50">
<i class="zmdi zmdi-search"></i>
<form action="#">
<div class="search-box__mdl-textfield mdl-textfield mdl-js-textfield w260" >
<input class="mdl-textfield__input search-box__search-field" type="text" id="main-search" value="' . $value . '">
<label class="mdl-textfield__label" for="sample1">Search...</label>
</div>
</form>
</div>';
}
$return .= '</div>';
$result = event(strtolower(class_basename($this->datatable)) . '.datatables.top.center', [&$return]);
$return .= isset($result[0]) ? $result[0] : '';
$return .= '<div class="card-ctrls__right">';
if (!is_null($this->rightCtrls)) {
$return .= $this->rightCtrls;
}
if (empty($filters) and ! is_null($this->datatable)) {
$list = $this->datatable->getFilters();
foreach ($list as $filter) {
$this->filterAdapter->add($filter);
}
$filters = $this->filterAdapter->getFilters();
}
if ($hasColumnFilter) {
$return .= $this->getColumnFilterAdapter();
}
if ($filters) {
$return .= '<div id="filter-save-url" data-url=' . handles('antares/foundation::datatables/filters/save') . '></div><div class="ddown ctrls__right ml10 ">' . $filters . '</div>';
}
if ($hasMassActions) {
$return .= '
<div class="ddown ddown--left">
<div class="ddown__init btn--dropdown btn btn--capitalize btn--md btn--primary mdl-js-button is-disabled ml8" id="table-ma" disabled>' . trans('antares/foundation::label.with_selected') . '</div>
<div class="ddown__content">
<div class="ddown__arrow"></div>
<ul class="ddown__menu">';
foreach ($this->massActions as $massAction) {
$return .= '<li>' . $massAction->get() . '</li>';
}
$return .= '</ul></div></div>';
}
return $return . '</div></div>';
}
/**
* Adds right ctrls container
*
* @param String $rightCtrls
* @return $this
*/
public function addRightCtrls($rightCtrls = null)
{
if (!is_string($rightCtrls)) {
return $this;
}
$this->rightCtrls = $rightCtrls;
return $this;
}
/**
* Adds column filter adapter instance
*
* @return $this
*/
public function addColumnFilter()
{
array_set($this->behaviors, 'column_filters', true);
return $this;
}
/**
* Whether datatable has colum filter
*
* @return boolean
*/
protected function hasColumnFilter()
{
return array_get($this->behaviors, 'column_filters', false);
}
/**
* Gets colum filter adapter instance
*
* @return ColumnFilterAdapter
*/
protected function getColumnFilterAdapter()
{
if (is_null($this->columnFilterAdapter)) {
$this->columnFilterAdapter = app(ColumnFilterAdapter::class, [$this->collection, get_class($this->datatable)]);
}
return $this->columnFilterAdapter;
}
/**
* whether datatable has mass actions
*
* @return boolean
*/
protected function hasMassActions()
{
return !empty($this->massActions) and $this->massable;
}
/**
* add filter to datatables
*
* @param String $classname
* @return \Antares\Datatables\Html\Builder
*/
public function addFilter($classname, $query)
{
$filterClass = is_object($classname) ? get_class($classname) : $classname;
if (!class_exists($filterClass)) {
return $this;
}
$filter = str_slug(class_basename($filterClass));
$path = uri();
$this->dispatcher->fire("datatables:filters.{$path}.{$filter}.before", [$this->filterAdapter, $query]);
$this->filterAdapter->add($classname);
$this->dispatcher->fire("datatables:filters.{$path}.{$filter}.after", [$this->filterAdapter, $query]);
return $this;
}
/**
* triggers after generates table
*
* @return string
*/
protected function afterTable()
{
return '</div>';
}
/**
* defered data setter
*
* @param array | mixed $data
* @param mixed $count
* @return Builder
*/
public function setDeferedData()
{
if (request()->has('search')) {
return $this;
}
$totalItemsCount = $this->datatable->count();
$this->deferredData = $this->datatable->ajax()->getData()->data;
$filters = $this->datatable->getFilters();
$query = $this->datatable->query();
foreach ($filters as $filter) {
$this->addFilter($filter, $query);
}
$this->attributes = array_merge($this->attributes, [
"deferLoading" => $totalItemsCount,
'iDisplayLength' => $this->datatable->getPerPage()
]);
return $this;
}
/**
* Generate DataTable's table html.
*
* @param array $attributes
* @return string
*/
public function tableDeferred(array $attributes = [])
{
$string = '<thead><tr>';
foreach ($this->collection as $collectedItem) {
$columnAttributes = array_only($collectedItem->toArray(), ['class', 'id', 'width', 'style', 'data-class', 'data-hide']);
$columnAttributes['class'] = (isset($collectedItem->bolded) and $collectedItem->bolded === true) ? 'bolded' : array_get($columnAttributes, 'class', '');
$params = $this->html->attributes($columnAttributes);
$string .= "<th " . $params . ">{$collectedItem->title}</th>";
}
$string .= '</tr></thead>';
$string .= '<tbody>';
foreach ($this->getDeferredDataItems() as $item) {
$string .= '<tr>';
foreach ($this->collection as $collectedItem) {
$value = isset($item->{$collectedItem->data}) ? $item->{$collectedItem->data} : '---';
$string .= "<td>{$value}</td>";
}
$string .= '</tr>';
}
$string .= '</tbody>';
$this->tableAttributes = array_merge($this->tableAttributes, $attributes);
$scrollable = '';
if ($this->datatable->count() > 10 or strlen($this->ajax) > 0) {
$scrollable = 'data-scrollable';
}
$massable = (int) $this->hasMassActions();
return $this->tableInit() . $this->beforeTable() . '<table data-massable="' . $massable . '" ' . $scrollable . ' data-table-init = "true" ' . $this->html->attributes($this->tableAttributes) . '>' . $string . '</table>' . $this->afterTable();
}
/**
* Disable javascript scripts
*
* @return $this
*/
public function disableScripts()
{
$this->disabledScripts = true;
return $this;
}
/**
* Inits table container
*
* @return String
*/
protected function tableInit(): String
{
$attributes = $this->html->attributes($this->containerAttributes);
return "<div {$attributes}>";
}
/**
* @return array
*/
protected function getDeferredDataItems()
{
if ($this->deferredData instanceof JsonResponse) {
return $this->deferredData->getData()->data;
}
return $this->deferredData;
}
/**
* Container attributes setter
*
* @param array $attributes
* @return $this
*/
public function containerAttributes(array $attributes = []): Builder
{
foreach ($this->containerAttributes as $name => $value) {
if (is_null($param = array_get($attributes, $name))) {
continue;
}
array_set($this->containerAttributes, $name, $value . ' ' . $param);
}
return $this;
}
/**
* Table attributes setter
*
* @param array $attributes
* @return $this
*/
public function tableAttributes(array $attributes = array()): Builder
{
array_set($attributes, 'id', array_get($attributes, 'id') . str_random(3));
$this->tableAttributes = array_merge($this->tableAttributes, $attributes);
return $this;
}
/**
* Add a column in collection using attributes.
*
* @param array $attributes
* @return $this
*/
public function addColumn(array $attributes)
{
$query = $this->getQuery();
$orders = $query instanceof \Illuminate\Database\Eloquent\Builder ? $query->getQuery()->orders : null;
if (!isset($this->attributes['order']) and ! is_null($query) and ! is_null($orders)) {
foreach ($orders as $order) {
if ($order['column'] != $attributes['name']) {
continue;
}
$this->attributes['order'] = [
$this->collection->count(), $order['direction']
];
}
}
$path = uri();
$this->dispatcher->fire('datatables:' . $path . ':column.' . $attributes['name'], [&$attributes]);
$this->collection->push(new Column($attributes));
$this->dispatcher->fire('datatables:' . $path . ':after.' . $attributes['name'], $this);
return $this;
}
/**
* datatable name setter
*
* @param String $name
* @return \Antares\Datatables\Html\Builder
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* filters adapter getter
*
* @return \Antares\Datatables\Adapter\FilterAdapter
*/
public function getFilterAdapter()
{
return $this->filterAdapter;
}
/**
* method setter
*
* @param String $method
* @return \Antares\Datatables\Html\Builder
*/
public function setMethod($method)
{
$this->method = $method;
return $this;
}
/**
* appends mass actions to mass actions container
*
* @param Expression $massAction
* @return \Antares\Datatables\Html\Builder
*/
public function addMassAction($name, Expression $massAction)
{
$model = $this->getQuery()->getModel();
$this->massActions = array_merge($this->massActions, (array) event('datatables:' . uri() . ':before.massactions.action.' . $name, [$this->massActions, $model], true));
if (empty($this->massActions)) {
$this->massActions = [];
}
array_push($this->massActions, $massAction);
$this->massActions = array_merge($this->massActions, (array) event('datatables:' . uri() . ':after.massactions.action.' . $name, [$this->massActions, $model], true));
$this->massActions = array_unique($this->massActions);
return $this;
}
/**
*
* @return array
*/
public function getRawData()
{
return $this->deferredData->getData();
}
/**
* query builder setter
*
* @param mixed $query
* @return \Antares\Datatables\Html\Builder
*/
public function setQuery($query)
{
$this->query = $query;
return $this;
}
/**
* query builder getter
*
* @return mixed
*/
public function getQuery()
{
return $this->query;
}
/**
* defered data getter
*
* @return mixed
*/
public function getDeferedData()
{
return $this->deferredData;
}
/**
* Defered data setter
*
* @param type $datatable
* @return \Antares\Datatables\Html\Builder
*/
public function setDataTable($datatable)
{
$this->setQuery($datatable->query());
$this->datatable = $datatable;
return $this;
}
/**
* Add additional table selects
*
* @param String $html
* @return \Antares\Datatables\Html\Builder
*/
public function addGroupSelect($options, $columnIndex = 0, $defaultSelected = null, array $attributes = [])
{
$orderAdapter = app(OrderAdapter::class)->setClassname(get_class($this->datatable));
if (($order = $orderAdapter->getSelected()) !== false) {
$this->parameters([
'order' => [[$order['column'], $order['dir']]],
]);
}
$groupsFilter = app(GroupsFilterAdapter::class)->setClassname(get_class($this->datatable))->setIndex($columnIndex);
$data = ($options instanceof Collection) ? $options->toArray() : $options;
$id = array_get($this->tableAttributes, 'id') . '-filter-group';
if (is_null($value = $groupsFilter->getSelected($columnIndex))) {
$value = $defaultSelected;
}
$decorated = $this->html->decorate($attributes, [
'data-selectAR--mdl-big' => "true",
'class' => 'select2--prefix',
'id' => $id
]);
$html = Form::select('category', $data, $value, $decorated);
$groupsFilter->scripts(array_get($decorated, 'id'), $columnIndex);
array_push($this->selects, $html);
return $this;
}
/**
* Configure DataTable's parameters.
*
* @param array $attributes
* @return $this
*/
public function parameters(array $attributes = [])
{
if (!is_null($defaultOrder = array_get($attributes, 'order'))) {
$orderAdapter = app(OrderAdapter::class)->setClassname(get_class($this->datatable));
if (($order = $orderAdapter->getSelected()) !== false) {
$attributes = array_merge($attributes, [
'order' => [[$order['column'], $order['dir']]],
]);
}
}
return parent::parameters($attributes);
}
/**
* Generate DataTable javascript.
*
* @param null $script
* @param array $attributes
* @return string
*/
public function scripts($script = null, array $attributes = ['type' => 'text/javascript'])
{
if ($this->disabledScripts) {
return false;
}
$container = app('antares.asset')->container('antares/foundation::scripts');
$container->add('context_menu', '/packages/core/js/contextMenu.js');
$script = $script ?: $this->generateScripts();
if (app('request')->ajax()) {
return '<script' . $this->html->attributes($attributes) . '>' . $script . '</script>' . PHP_EOL;
} else {
return $container->inlineScript($this->tableAttributes['id'], $script);
}
}
}