Skip to content

Commit bd8f29c

Browse files
authored
Add ViewModeSwitcher control (#392)
A `ViewModeSwitcher` class similar to the one in icingadb-web is added. It was reworked so that adding and removing view modes can be done without declaring a subclass. To allow this the `ViewMode` class is added, which contains name, icon, titles for active/inactive state and the default page size. The `Controls` trait is added with a `createViewModeSwitcher()` function. It registers a listener to `ViewModeSwitcher::ON_REQUEST`, which populates the `ViewModeSwitcher` with the view mode from the url and emits `ON_VIEW_MODE_SET`. If the view mode is changed, a `ViewModeSwitcher::ON_SUBMIT` listener emits `ON_VIEW_MODE_CHANGE` with the `ViewModeSwitcher`, its previous view mode and a `$redirectUrl` that listeners may modify. The `handleControls()` function suggested in #340 is implemented, which calls `handleRequest()` for all controls passed to `trackControl()` , in case of the `ViewModeSwitcher` this is done by the factory method. The `CompatController` registers listeners on the trait's events in the `createLimitControl()` and `createPaginationControl()` factories, to adjust the default limit, page size to teh current view mode, and correct the current page when the view mode is changed. `SearchControls::createSearchBar()` is adjusted to shift the view mode param before building its filter, because the `Controls` trait does not have access to the `UrlParams`.
2 parents cc9077d + 98e0b90 commit bd8f29c

8 files changed

Lines changed: 1058 additions & 4 deletions

File tree

asset/css/controls.less

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@
109109
/**
110110
The default layout of list controls in Icinga Web
111111
112-
┌────────────────────────────────────────────────────────────────┐
113-
│ .pagination-control .limit-control .sort-control │
114-
│ <-------------------- .search-controls ----------------------> │
115-
└────────────────────────────────────────────────────────────────┘
112+
┌───────────────────────────────────────────────────────────────────────────────────
113+
│ .pagination-control .view-mode-switcher .limit-control .sort-control │
114+
│ <---------------------------- .search-controls ---------------------------------> │
115+
└───────────────────────────────────────────────────────────────────────────────────
116116
*/
117117
.controls.default-layout {
118118
.box-shadow(0, 0, 0, 1px, @controls-separator-bg);
@@ -127,10 +127,12 @@
127127
}
128128

129129
> .sort-control,
130+
> .view-mode-switcher,
130131
> .limit-control {
131132
float: right;
132133
}
133134

135+
> .view-mode-switcher,
134136
> .limit-control {
135137
margin-right: .5em;
136138
}

asset/css/view-mode-switcher.less

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.view-mode-switcher {
2+
display: flex;
3+
4+
input {
5+
display: none;
6+
}
7+
8+
label {
9+
color: var(--control-color, @control-color);
10+
line-height: 1;
11+
background: var(--default-input-bg, @default-input-bg);
12+
padding: 14/16*.25em 14/16*.5em;
13+
font-size: 16/12em;
14+
height: 24/16em; // desired pixel height / font-size
15+
cursor: pointer;
16+
17+
&:first-of-type {
18+
border-top-left-radius: 0.25em;
19+
border-bottom-left-radius: 0.25em;
20+
}
21+
22+
&:last-of-type {
23+
border-top-right-radius: 0.25em;
24+
border-bottom-right-radius: 0.25em;
25+
}
26+
27+
&:not(:last-of-type) {
28+
border-right: 1px solid var(--default-input-hover-bg, @default-input-hover-bg);
29+
}
30+
31+
i {
32+
// fix height for Chrome
33+
display: block;
34+
}
35+
}
36+
37+
input[checked] + label {
38+
background-color: var(--control-color, @control-color);
39+
color: var(--default-text-color-inverted, @default-text-color-inverted);
40+
cursor: default;
41+
}
42+
}

src/Common/Controls.php

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
namespace ipl\Web\Common;
4+
5+
use ipl\Html\Form;
6+
use ipl\Stdlib\Events;
7+
use ipl\Web\Control\ViewModeSwitcher;
8+
use ipl\Web\Url;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
11+
/**
12+
* Provides factory methods to prepare reusable web controls and allows to handle their requests
13+
*
14+
* {@see static::handleControls()} can be called to call {@see Form::handleRequest()} for all Controls created by
15+
* this trait.
16+
*/
17+
trait Controls
18+
{
19+
use Events;
20+
21+
/** @var string Event emitted when the view mode has been changed */
22+
public const ON_VIEW_MODE_CHANGE = 'view-mode-change';
23+
24+
/** @var string Event emmitted when the created {@see ViewModeSwitcher} is populated with a view mode */
25+
public const ON_VIEW_MODE_SET = 'view-mode-set';
26+
27+
/** @var array<Form> Controls for which {@see self::handleControls()} must call {@see Form::handleRequest()} */
28+
private array $trackedControls = [];
29+
30+
/**
31+
* Redirect to the given Url
32+
*
33+
* @param Url|string $url
34+
*
35+
* @return never
36+
*/
37+
abstract protected function redirectNow($url);
38+
39+
/**
40+
* Create a {@see ViewModeSwitcher} control
41+
*
42+
* On {@see ViewModeSwitcher::ON_REQUEST} the created instance is populated with the view mode from the url
43+
* and {@see static::ON_VIEW_MODE_SET} is emmitted.
44+
*
45+
* On {@see ViewModeSwitcher::ON_SUBMIT}, if the view mode was changed, {@see static::ON_VIEW_MODE_CHANGE}
46+
* is emmitted with the {@see ViewModeSwitcher}, its previous view mode and a redirect url, which listeners
47+
* may modify.
48+
*
49+
* @return ViewModeSwitcher
50+
*/
51+
public function createViewModeSwitcher(): ViewModeSwitcher
52+
{
53+
$viewModeSwitcher = new ViewModeSwitcher();
54+
55+
$this->trackControl($viewModeSwitcher);
56+
57+
$viewModeSwitcher->on(
58+
ViewModeSwitcher::ON_REQUEST,
59+
function (
60+
ServerRequestInterface $request,
61+
ViewModeSwitcher $viewModeSwitcher
62+
) {
63+
$viewModeSwitcher->populate([
64+
$viewModeSwitcher->getViewModeParam()
65+
=> $request->getQueryParams()[$viewModeSwitcher->getViewModeParam()] ?? null
66+
]);
67+
68+
$this->emit(static::ON_VIEW_MODE_SET, [$viewModeSwitcher]);
69+
}
70+
);
71+
72+
$viewModeSwitcher->on(
73+
ViewModeSwitcher::ON_SUBMIT,
74+
function (
75+
ViewModeSwitcher $viewModeSwitcher
76+
): void {
77+
$previousViewModeName =
78+
$viewModeSwitcher->getRequest()->getQueryParams()[$viewModeSwitcher->getViewModeParam()]
79+
?? $viewModeSwitcher->getDefaultViewMode()->getName();
80+
$previousViewMode = $viewModeSwitcher->getViewModes()[$previousViewModeName];
81+
82+
if ($viewModeSwitcher->getViewMode()->getName() !== $previousViewMode->getName()) {
83+
$redirectUrl = Url::fromRequest();
84+
$this->emit(static::ON_VIEW_MODE_CHANGE, [$viewModeSwitcher, $previousViewMode, $redirectUrl]);
85+
$redirectUrl->setParam(
86+
$viewModeSwitcher->getViewModeParam(),
87+
$viewModeSwitcher->getViewMode()->getName()
88+
);
89+
$this->redirectNow($redirectUrl);
90+
}
91+
}
92+
);
93+
94+
return $viewModeSwitcher;
95+
}
96+
97+
/**
98+
* Register the given control for lookup by {@see self::getTrackedControl()} and for {@see self::handleControls()}
99+
*
100+
* @param Form $control
101+
*
102+
* @return $this
103+
*/
104+
protected function trackControl(Form $control): static
105+
{
106+
$this->trackedControls[] = $control;
107+
108+
return $this;
109+
}
110+
111+
/**
112+
* Get the tracked control of the given type
113+
*
114+
* @template TControl of Form
115+
*
116+
* @param class-string<TControl> $type
117+
*
118+
* @return ?TControl
119+
*/
120+
protected function getTrackedControl(string $type): ?Form
121+
{
122+
foreach ($this->trackedControls as $control) {
123+
if ($control instanceof $type) {
124+
return $control;
125+
}
126+
}
127+
128+
return null;
129+
}
130+
131+
/**
132+
* Call {@see Form::handleRequest()} on every control in {@see self::$trackedControls}.
133+
*
134+
* @param ServerRequestInterface $request
135+
*
136+
* @return $this
137+
*/
138+
protected function handleControls(ServerRequestInterface $request): static
139+
{
140+
foreach ($this->trackedControls as $control) {
141+
$control->handleRequest($request);
142+
}
143+
144+
return $this;
145+
}
146+
}

src/Compat/CompatController.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
use ipl\Html\ValidHtml;
1313
use ipl\Orm\Query;
1414
use ipl\Stdlib\Contract\Paginatable;
15+
use ipl\Stdlib\Events;
1516
use ipl\Web\Control\LimitControl;
1617
use ipl\Web\Control\PaginationControl;
1718
use ipl\Web\Control\SearchBar;
1819
use ipl\Web\Control\SortControl;
20+
use ipl\Web\Control\ViewModeSwitcher;
21+
use ipl\Web\Control\ViewModeSwitcher\ViewMode;
1922
use ipl\Web\Layout\Content;
2023
use ipl\Web\Layout\Controls;
2124
use ipl\Web\Layout\Footer;
@@ -26,6 +29,8 @@
2629

2730
class CompatController extends Controller
2831
{
32+
use Events;
33+
2934
/** @var Content */
3035
protected $content;
3136

@@ -244,6 +249,10 @@ protected function addTitleTab($title, ...$args)
244249
*
245250
* This automatically shifts the limit URL parameter from {@link $params}.
246251
*
252+
* If a {@see ViewModeSwitcher} is created using {@see \ipl\Web\Common\Controls::createViewModeSwitcher()}
253+
* the default limit will automatically be set to the current {@see ViewMode::getPageSize()},
254+
* unless an explicit limit is declared in the url.
255+
*
247256
* @return LimitControl
248257
*/
249258
public function createLimitControl(): LimitControl
@@ -253,6 +262,17 @@ public function createLimitControl(): LimitControl
253262

254263
$this->params->shift($limitControl->getLimitParam());
255264

265+
$this->on(
266+
/** {@see \ipl\Web\Common\Controls::ON_VIEW_MODE_SET} */
267+
'view-mode-set',
268+
function (ViewModeSwitcher $viewModeSwitcher) use ($limitControl) {
269+
if (! isset($this->getServerRequest()->getQueryParams()[$limitControl->getLimitParam()])) {
270+
$limit = $viewModeSwitcher->getViewMode()->getPageSize();
271+
$limitControl->setDefaultLimit($this->getPageSize($limit));
272+
}
273+
}
274+
);
275+
256276
return $limitControl;
257277
}
258278

@@ -261,6 +281,13 @@ public function createLimitControl(): LimitControl
261281
*
262282
* This automatically shifts the pagination URL parameters from {@link $params}.
263283
*
284+
* If a {@see ViewModeSwitcher} is created using {@see \ipl\Web\Common\Controls::createViewModeSwitcher()}
285+
* the default page size will automatically be set to the current {@see ViewMode::getPageSize()},
286+
* unless an explicit page size is declared in the url.
287+
*
288+
* When a change of view mode changes the page size, the current page is corrected so that the previously first
289+
* element is always visible on the corrected page.
290+
*
264291
* @param Paginatable $paginatable
265292
*
266293
* @return PaginationControl
@@ -274,6 +301,44 @@ public function createPaginationControl(Paginatable $paginatable): PaginationCon
274301
$this->params->shift($paginationControl->getPageParam());
275302
$this->params->shift($paginationControl->getPageSizeParam());
276303

304+
$this->on(
305+
/** {@see \ipl\Web\Common\Controls::ON_VIEW_MODE_SET} */
306+
'view-mode-set',
307+
function (ViewModeSwitcher $viewModeSwitcher) use ($paginationControl) {
308+
if (! isset($this->getServerRequest()->getQueryParams()[$paginationControl->getPageSizeParam()])) {
309+
$limit = $viewModeSwitcher->getViewMode()->getPageSize();
310+
$paginationControl->setDefaultPageSize($this->getPageSize($limit))
311+
->apply();
312+
}
313+
}
314+
);
315+
316+
$this->on(
317+
/** {@see \ipl\Web\Common\Controls::ON_VIEW_MODE_CHANGE} */
318+
'view-mode-change',
319+
function (
320+
ViewModeSwitcher $viewModeSwitcher,
321+
ViewMode $previousViewMode,
322+
Url $redirectUrl
323+
) use ($paginationControl) {
324+
if (
325+
! isset($this->getServerRequest()->getQueryParams()[$paginationControl->getPageSizeParam()])
326+
&& $viewModeSwitcher->getViewMode()->getName() !== $previousViewMode->getName()
327+
) {
328+
$previous = $previousViewMode->getPageSize();
329+
$new = $viewModeSwitcher->getViewMode()->getPageSize();
330+
331+
$page = (int) floor((($paginationControl->getCurrentPageNumber() - 1) * $previous) / $new) + 1;
332+
333+
if ($page > 1) {
334+
$redirectUrl->setParam($paginationControl->getPageParam(), $page);
335+
} else {
336+
$redirectUrl->remove($paginationControl->getPageParam());
337+
}
338+
}
339+
}
340+
);
341+
277342
return $paginationControl->apply();
278343
}
279344

src/Compat/SearchControls.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
use ipl\Orm\Exception\InvalidRelationException;
88
use ipl\Orm\Query;
99
use ipl\Stdlib\Seq;
10+
use ipl\Web\Common\Controls;
1011
use ipl\Web\Control\SearchBar;
1112
use ipl\Web\Control\SearchEditor;
13+
use ipl\Web\Control\ViewModeSwitcher;
1214
use ipl\Web\Filter\QueryString;
1315
use ipl\Web\Url;
1416
use ipl\Stdlib\Filter;
@@ -65,6 +67,14 @@ public function createSearchBar(Query $query, ...$params): SearchBar
6567
$redirectUrl = $requestUrl->onlyWith($preserveParams);
6668
}
6769

70+
/** If {@see Controls::createViewModeSwitcher()} was used, the view mode param must be removed form the url */
71+
if (method_exists($this, 'getTrackedControl')) {
72+
$viewModeSwitcher = $this->getTrackedControl(ViewModeSwitcher::class);
73+
if ($viewModeSwitcher !== null) {
74+
$this->params->shift($viewModeSwitcher->getViewModeParam());
75+
}
76+
}
77+
6878
$filter = QueryString::fromString((string) $this->params)
6979
->on(QueryString::ON_CONDITION, function (Filter\Condition $condition) use ($query) {
7080
$this->enrichFilterCondition($condition, $query);

0 commit comments

Comments
 (0)