1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* HiPanel core package |
4
|
|
|
* |
5
|
|
|
* @link https://hipanel.com/ |
6
|
|
|
* @package hipanel-core |
7
|
|
|
* @license BSD-3-Clause |
8
|
|
|
* @copyright Copyright (c) 2014-2019, HiQDev (http://hiqdev.com/) |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace hipanel\widgets; |
12
|
|
|
|
13
|
|
|
use hipanel\assets\StickySidebarAsset; |
14
|
|
|
use hipanel\grid\RepresentationCollectionFinder; |
15
|
|
|
use hipanel\helpers\ArrayHelper; |
16
|
|
|
use hipanel\models\IndexPageUiOptions; |
17
|
|
|
use hiqdev\higrid\representations\RepresentationCollectionInterface; |
18
|
|
|
use hiqdev\yii2\export\widgets\IndexPageExportLinks; |
19
|
|
|
use Yii; |
20
|
|
|
use yii\base\InvalidParamException; |
21
|
|
|
use yii\base\Model; |
22
|
|
|
use yii\base\Widget; |
23
|
|
|
use yii\bootstrap\ButtonDropdown; |
24
|
|
|
use yii\data\DataProviderInterface; |
25
|
|
|
use yii\helpers\Html; |
26
|
|
|
use yii\helpers\Inflector; |
27
|
|
|
use yii\helpers\Json; |
28
|
|
|
use yii\helpers\Url; |
29
|
|
|
use yii\web\JsExpression; |
30
|
|
|
use yii\web\View; |
31
|
|
|
|
32
|
|
|
class IndexPage extends Widget |
33
|
|
|
{ |
34
|
|
|
/** |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
protected $_layout; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var Model the search model |
41
|
|
|
*/ |
42
|
|
|
public $model; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var IndexPageUiOptions |
46
|
|
|
*/ |
47
|
|
|
private $uiModel; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var object original view context. |
51
|
|
|
* It is used to render sub-views with the same context, as IndexPage |
52
|
|
|
*/ |
53
|
|
|
public $originalContext; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @var DataProviderInterface |
57
|
|
|
*/ |
58
|
|
|
public $dataProvider; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var array Hash of document blocks, that can be rendered later in the widget's views |
62
|
|
|
* Blocks can be set explicitly on widget initialisation, or by calling [[beginContent]] and |
63
|
|
|
* [[endContent]] |
64
|
|
|
* |
65
|
|
|
* @see beginContent |
66
|
|
|
* @see endContent |
67
|
|
|
*/ |
68
|
|
|
public $contents = []; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var string the name of current content block, that is under the render |
72
|
|
|
* @see beginContent |
73
|
|
|
* @see endContent |
74
|
|
|
*/ |
75
|
|
|
protected $_current = null; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @var array |
79
|
|
|
*/ |
80
|
|
|
public $searchFormData = []; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* @var array |
84
|
|
|
*/ |
85
|
|
|
public $searchFormOptions = []; |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* @var string the name of view file that contains search fields for the index page. Defaults to `_search` |
89
|
|
|
* @see renderSearchForm() |
90
|
|
|
*/ |
91
|
|
|
public $searchView = '_search'; |
92
|
|
|
|
93
|
|
|
/** {@inheritdoc} */ |
94
|
|
|
public function init() |
95
|
|
|
{ |
96
|
|
|
parent::init(); |
97
|
|
|
$searchFormId = Json::htmlEncode("#{$this->getBulkFormId()}"); |
98
|
|
|
$this->originalContext = Yii::$app->view->context; |
99
|
|
|
$view = $this->getView(); |
100
|
|
|
// Fix a very narrow select2 input in the search tables |
101
|
|
|
$view->registerCss('#content-pjax .select2-dropdown--below { min-width: 170px!important; }'); |
102
|
|
|
$view->registerJs(<<<"JS" |
103
|
|
|
// Checkbox |
104
|
|
|
var bulkcontainer = $('.box-bulk-actions fieldset'); |
105
|
|
|
$($searchFormId).on('change', 'input[type="checkbox"]', function(event) { |
106
|
|
|
var checkboxes = $('input.grid-checkbox'); |
107
|
|
|
if (checkboxes.filter(':checked').length > 0) { |
108
|
|
|
bulkcontainer.prop('disabled', false); |
109
|
|
|
} else if (checkboxes.filter(':checked').length === 0) { |
110
|
|
|
bulkcontainer.prop('disabled', true); |
111
|
|
|
} |
112
|
|
|
}); |
113
|
|
|
// On/Off Actions TODO: reduce scope |
114
|
|
|
$(document).on('click', '.box-bulk-actions a', function (event) { |
115
|
|
|
var link = $(this); |
116
|
|
|
var action = link.data('action'); |
117
|
|
|
var form = $($searchFormId); |
118
|
|
|
if (action) { |
119
|
|
|
form.attr({'action': action, method: 'POST'}).submit(); |
120
|
|
|
} |
121
|
|
|
}); |
122
|
|
|
// Do not open select2 when it is clearing |
123
|
|
|
var comboSelector = 'div[role=grid] :input[data-combo-field], .advanced-search :input[data-combo-field]'; |
124
|
|
|
$(document).on('select2:unselecting', comboSelector, function(e) { |
125
|
|
|
$(e.target).data('unselecting', true); |
126
|
|
|
}).on('select2:open', comboSelector, function(e) { // note the open event is important |
127
|
|
|
var el = $(e.target); |
128
|
|
|
if (el.data('unselecting')) { |
129
|
|
|
el.removeData('unselecting'); // you need to unset this before close |
130
|
|
|
el.select2('close'); |
131
|
|
|
} |
132
|
|
|
}); |
133
|
|
|
JS |
134
|
|
|
); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
public function getUiModel() |
138
|
|
|
{ |
139
|
|
|
if ($this->uiModel === null) { |
140
|
|
|
$this->uiModel = $this->originalContext->indexPageUiOptionsModel; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
return $this->uiModel; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Begins output buffer capture to save data in [[contents]] with the $name key. |
148
|
|
|
* Must not be called nested. See [[endContent]] for capture terminating. |
149
|
|
|
* @param string $name |
150
|
|
|
*/ |
151
|
|
|
public function beginContent($name) |
152
|
|
|
{ |
153
|
|
|
if ($this->_current) { |
154
|
|
|
throw new InvalidParamException('Output buffer capture is already running for ' . $this->_current); |
|
|
|
|
155
|
|
|
} |
156
|
|
|
$this->_current = $name; |
157
|
|
|
ob_start(); |
158
|
|
|
ob_implicit_flush(false); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Terminates output buffer capture started by [[beginContent()]]. |
163
|
|
|
* @see beginContent |
164
|
|
|
*/ |
165
|
|
|
public function endContent() |
166
|
|
|
{ |
167
|
|
|
if (!$this->_current) { |
168
|
|
|
throw new InvalidParamException('Outout buffer capture is not running. Call beginContent() first'); |
|
|
|
|
169
|
|
|
} |
170
|
|
|
$this->contents[$this->_current] = ob_get_contents(); |
171
|
|
|
ob_end_clean(); |
172
|
|
|
$this->_current = null; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Returns content saved in [[content]] by $name. |
177
|
|
|
* @param string $name |
178
|
|
|
* @return string |
179
|
|
|
*/ |
180
|
|
|
public function renderContent($name) |
181
|
|
|
{ |
182
|
|
|
return $this->contents[$name]; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
public function run() |
186
|
|
|
{ |
187
|
|
|
$layout = $this->getLayout(); |
188
|
|
|
if ($layout === 'horizontal') { |
189
|
|
|
$this->horizontalClientScriptInit(); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
return $this->render($layout); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
private function horizontalClientScriptInit() |
196
|
|
|
{ |
197
|
|
|
$view = $this->getView(); |
198
|
|
|
StickySidebarAsset::register($view); |
199
|
|
|
$view->registerCss(<<<'CSS' |
200
|
|
|
.advanced-search[min-width~="150px"] form > div { |
201
|
|
|
width: 100%; |
202
|
|
|
position: inherit; |
203
|
|
|
} |
204
|
|
|
.horizontal-view .content-sidebar { |
205
|
|
|
will-change: min-height; |
206
|
|
|
} |
207
|
|
|
.horizontal-view .content-sidebar .content-sidebar__inner { |
208
|
|
|
transform: translate(0, 0); /* For browsers don't support translate3d. */ |
209
|
|
|
transform: translate3d(0, 0, 0); |
210
|
|
|
will-change: position, transform; |
211
|
|
|
} |
212
|
|
|
.horizontal-view .content-sidebar__inner > .btn, |
213
|
|
|
.horizontal-view .content-sidebar__inner .dropdown > .btn { |
214
|
|
|
display: block; |
215
|
|
|
margin-bottom: 10px; |
216
|
|
|
} |
217
|
|
|
CSS |
218
|
|
|
); |
219
|
|
|
$view->registerJs(<<<"JS" |
220
|
|
|
var isDesktop = $(window).innerWidth() > 750; |
221
|
|
|
if (isDesktop) { |
222
|
|
|
var stickySidebar = new StickySidebar('.horizontal-view .content-sidebar', { |
223
|
|
|
topSpacing: 10, |
224
|
|
|
bottomSpacing: 20, |
225
|
|
|
containerSelector: '.horizontal-content', |
226
|
|
|
innerWrapperSelector: '.content-sidebar__inner' |
227
|
|
|
}); |
228
|
|
|
$(document).on('pjax:end', stickySidebar.updateSticky); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
$(document).on('pjax:end', function() { |
232
|
|
|
$('.advanced-search form > div').css({'width': '100%'}); |
233
|
|
|
$(window).trigger('scroll'); // Fix left search block position |
234
|
|
|
}); |
235
|
|
|
JS |
236
|
|
|
, View::POS_LOAD); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
public function detectLayout() |
240
|
|
|
{ |
241
|
|
|
return $this->getUiModel()->orientation; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* @param array $data |
246
|
|
|
* @void |
247
|
|
|
*/ |
248
|
|
|
public function setSearchFormData($data = []) |
249
|
|
|
{ |
250
|
|
|
$this->searchFormData = $data; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* @param array $options |
255
|
|
|
* @void |
256
|
|
|
*/ |
257
|
|
|
public function setSearchFormOptions($options = []) |
258
|
|
|
{ |
259
|
|
|
$this->searchFormOptions = $options; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
public function renderSearchForm($advancedSearchOptions = []) |
263
|
|
|
{ |
264
|
|
|
$advancedSearchOptions = array_merge($advancedSearchOptions, $this->searchFormOptions); |
265
|
|
|
ob_start(); |
266
|
|
|
ob_implicit_flush(false); |
267
|
|
|
try { |
268
|
|
|
$search = $this->beginSearchForm($advancedSearchOptions); |
269
|
|
|
echo Yii::$app->view->render($this->searchView, array_merge(compact('search'), $this->searchFormData), $this->originalContext); |
270
|
|
|
$search->end(); |
271
|
|
|
} catch (\Exception $e) { |
272
|
|
|
ob_end_clean(); |
273
|
|
|
throw $e; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
return ob_get_clean(); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
public function beginSearchForm($options = []) |
280
|
|
|
{ |
281
|
|
|
return AdvancedSearch::begin(array_merge(['model' => $this->model], $options)); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
public function renderSearchButton() |
285
|
|
|
{ |
286
|
|
|
return AdvancedSearch::renderButton() . "\n"; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
public function renderLayoutSwitcher() |
290
|
|
|
{ |
291
|
|
|
return IndexLayoutSwitcher::widget(['uiModel' => $this->getUiModel()]); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
public function renderPerPage() |
295
|
|
|
{ |
296
|
|
|
$items = []; |
297
|
|
|
foreach ([25, 50, 100, 200, 500] as $pageSize) { |
298
|
|
|
$items[] = ['label' => $pageSize, 'url' => Url::current(['per_page' => $pageSize])]; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
return ButtonDropdown::widget([ |
302
|
|
|
'label' => Yii::t('hipanel', 'Per page') . ': ' . $this->getUiModel()->per_page, |
303
|
|
|
'options' => ['class' => 'btn-default btn-sm'], |
304
|
|
|
'dropdown' => [ |
305
|
|
|
'items' => $items, |
306
|
|
|
], |
307
|
|
|
]); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Renders button to choose representation. |
312
|
|
|
* Returns empty string when nothing to choose (less then 2 representations available). |
313
|
|
|
* |
314
|
|
|
* @param RepresentationCollectionInterface $collection |
315
|
|
|
* @return string rendered HTML |
316
|
|
|
*/ |
317
|
|
|
public function renderRepresentations($collection) |
318
|
|
|
{ |
319
|
|
|
$current = $this->getUiModel()->representation; |
320
|
|
|
|
321
|
|
|
$representations = $collection->getAll(); |
322
|
|
|
if (count($representations) < 2) { |
323
|
|
|
return ''; |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
$items = []; |
327
|
|
|
foreach ($representations as $name => $representation) { |
328
|
|
|
$items[] = [ |
329
|
|
|
'label' => $representation->getLabel(), |
330
|
|
|
'url' => Url::current(['representation' => $name]), |
331
|
|
|
]; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
return ButtonDropdown::widget([ |
335
|
|
|
'label' => Yii::t('hipanel:synt', 'View') . ': ' . $collection->getByName($current)->getLabel(), |
336
|
|
|
'options' => ['class' => 'btn-default btn-sm'], |
337
|
|
|
'dropdown' => [ |
338
|
|
|
'items' => $items, |
339
|
|
|
], |
340
|
|
|
]); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
public function renderSorter(array $options) |
344
|
|
|
{ |
345
|
|
|
return LinkSorter::widget(array_merge([ |
346
|
|
|
'show' => true, |
347
|
|
|
'uiModel' => $this->getUiModel(), |
348
|
|
|
'dataProvider' => $this->dataProvider, |
349
|
|
|
'sort' => $this->dataProvider->getSort(), |
350
|
|
|
'buttonClass' => 'btn btn-default dropdown-toggle btn-sm', |
351
|
|
|
], $options)); |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
public function renderExport() |
355
|
|
|
{ |
356
|
|
|
$isGridExportActionExists = (bool) Yii::$app->controller->createAction('export'); |
357
|
|
|
/** @var RepresentationCollectionFinder $repColFinder */ |
358
|
|
|
$repColFinder = Yii::createObject(RepresentationCollectionFinder::class); |
359
|
|
|
$collection = $repColFinder->findOrFallback(); |
360
|
|
|
$isRepresentationExists = count($collection->getAll()) > 0; |
361
|
|
|
if ($isGridExportActionExists && $isRepresentationExists) { |
362
|
|
|
return IndexPageExportLinks::widget(); |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
public function getViewPath() |
367
|
|
|
{ |
368
|
|
|
return parent::getViewPath() . DIRECTORY_SEPARATOR . (new \ReflectionClass($this))->getShortName(); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
public function getBulkFormId() |
372
|
|
|
{ |
373
|
|
|
return 'bulk-' . Inflector::camel2id($this->model->formName()); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
public function beginBulkForm($action = '') |
377
|
|
|
{ |
378
|
|
|
echo Html::beginForm($action, 'POST', ['id' => $this->getBulkFormId()]); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
public function endBulkForm() |
382
|
|
|
{ |
383
|
|
|
echo Html::endForm(); |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* @param string|array $action |
388
|
|
|
* @param string $text |
389
|
|
|
* @param array $options |
390
|
|
|
* @return string |
391
|
|
|
*/ |
392
|
|
|
public function renderBulkButton($action, $text, array $options = []): string |
393
|
|
|
{ |
394
|
|
|
$color = ArrayHelper::remove($options, 'color', 'default'); |
395
|
|
|
$confirm = ArrayHelper::remove($options, 'confirm', false); |
396
|
|
|
if ($confirm) { |
397
|
|
|
$options['onclick'] = new JsExpression("return confirm('{$confirm}');"); |
398
|
|
|
} |
399
|
|
|
$defaultOptions = [ |
400
|
|
|
'class' => "btn btn-$color btn-sm", |
401
|
|
|
'form' => $this->getBulkFormId(), |
402
|
|
|
'formmethod' => 'POST', |
403
|
|
|
'formaction' => Url::toRoute($action), |
404
|
|
|
]; |
405
|
|
|
|
406
|
|
|
return Html::submitButton($text, array_merge($defaultOptions, $options)); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* @param string $permission |
411
|
|
|
* @param string $button |
412
|
|
|
* @return string|null |
413
|
|
|
*/ |
414
|
|
|
public function withPermission(string $permission, string $button): ?string |
415
|
|
|
{ |
416
|
|
|
if (Yii::$app->user->can($permission)) { |
417
|
|
|
return $button; |
418
|
|
|
} |
419
|
|
|
return null; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* @param string|array $action |
424
|
|
|
* @param string|null $text |
425
|
|
|
* @param array $options |
426
|
|
|
* @return string |
427
|
|
|
*/ |
428
|
|
|
public function renderBulkDeleteButton($action, $text = null, array $options = []): string |
429
|
|
|
{ |
430
|
|
|
$text = $text ?? Yii::t('hipanel', 'Delete'); |
431
|
|
|
$options['color'] = $options['color'] ?? 'danger'; |
432
|
|
|
$options['confirm'] = $options['confirm'] ?? Yii::t('hipanel', 'Are you sure you want to delete these items?'); |
433
|
|
|
|
434
|
|
|
return $this->renderBulkButton($action, $text, $options); |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* @return string |
439
|
|
|
*/ |
440
|
|
|
public function getLayout() |
441
|
|
|
{ |
442
|
|
|
if ($this->_layout === null) { |
443
|
|
|
$this->_layout = $this->detectLayout(); |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
return $this->_layout; |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* @param string $layout |
451
|
|
|
*/ |
452
|
|
|
public function setLayout($layout) |
453
|
|
|
{ |
454
|
|
|
$this->_layout = $layout; |
455
|
|
|
} |
456
|
|
|
} |
457
|
|
|
|
This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.