Passed
Pull Request — master (#148)
by Sergei
02:47
created

WebView::setCssStrings()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 10
cc 4
nc 2
nop 1
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\View;
6
7
use InvalidArgumentException;
8
use Yiisoft\Html\Html;
9
use Yiisoft\Html\Tag\Script;
10
use Yiisoft\Html\Tag\Style;
11
use Yiisoft\Json\Json;
12
use Yiisoft\View\Event\BodyBegin;
13
use Yiisoft\View\Event\BodyEnd;
14
use Yiisoft\View\Event\PageEnd;
15
16
use function array_key_exists;
17
use function get_class;
18
use function gettype;
19
use function in_array;
20
use function is_array;
21
use function is_object;
22
use function is_string;
23
24
/**
25
 * View represents a view object in the MVC pattern.
26
 *
27
 * View provides a set of methods (e.g. {@see render()} for rendering purpose.
28
 *
29
 * You can modify its configuration by adding an array to your application config under `components` as it is shown in
30
 * the following example:
31
 *
32
 * ```php
33
 * 'view' => [
34
 *     'theme' => 'app\themes\MyTheme',
35
 *     'renderers' => [
36
 *         // you may add Smarty or Twig renderer here
37
 *     ]
38
 *     // ...
39
 * ]
40
 * ```
41
 *
42
 * For more details and usage information on View, see the [guide article on views](guide:structure-views).
43
 */
44
class WebView extends View
45
{
46
    /**
47
     * The location of registered JavaScript code block or files.
48
     * This means the location is in the head section.
49
     */
50
    public const POSITION_HEAD = 1;
51
52
    /**
53
     * The location of registered JavaScript code block or files.
54
     * This means the location is at the beginning of the body section.
55
     */
56
    public const POSITION_BEGIN = 2;
57
58
    /**
59
     * The location of registered JavaScript code block or files.
60
     * This means the location is at the end of the body section.
61
     */
62
    public const POSITION_END = 3;
63
64
    /**
65
     * The location of registered JavaScript code block.
66
     * This means the JavaScript code block will be executed when HTML document composition is ready.
67
     */
68
    public const POSITION_READY = 4;
69
70
    /**
71
     * The location of registered JavaScript code block.
72
     * This means the JavaScript code block will be executed when HTML page is completely loaded.
73
     */
74
    public const POSITION_LOAD = 5;
75
76
    private const DEFAULT_POSITION_CSS_FILE = self::POSITION_HEAD;
77
    private const DEFAULT_POSITION_CSS_STRING = self::POSITION_HEAD;
78
    private const DEFAULT_POSITION_JS_FILE = self::POSITION_END;
79
    private const DEFAULT_POSITION_JS_VARIABLE = self::POSITION_HEAD;
80
    private const DEFAULT_POSITION_JS_STRING = self::POSITION_END;
81
82
    /**
83
     * This is internally used as the placeholder for receiving the content registered for the head section.
84
     */
85
    private const PLACEHOLDER_HEAD = '<![CDATA[YII-BLOCK-HEAD-%s]]>';
86
87
    /**
88
     * This is internally used as the placeholder for receiving the content registered for the beginning of the body
89
     * section.
90
     */
91
    private const PLACEHOLDER_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN-%s]]>';
92
93
    /**
94
     * This is internally used as the placeholder for receiving the content registered for the end of the body section.
95
     */
96
    private const PLACEHOLDER_BODY_END = '<![CDATA[YII-BLOCK-BODY-END-%s]]>';
97
98
    /**
99
     * @var string the page title
100
     */
101
    private string $title = '';
102
103
    /**
104
     * @var array the registered meta tags.
105
     *
106
     * {@see registerMetaTag()}
107
     */
108
    private array $metaTags = [];
109
110
    /**
111
     * @var array the registered link tags.
112
     *
113
     * {@see registerLinkTag()}
114
     */
115
    private array $linkTags = [];
116
117
    /**
118
     * @var array the registered CSS code blocks.
119
     *
120
     * {@see registerCss()}
121
     */
122
    private array $css = [];
123
124
    /**
125
     * @var array the registered CSS files.
126
     *
127
     * {@see registerCssFile()}
128
     */
129
    private array $cssFiles = [];
130
131
    /**
132
     * @var array the registered JS code blocks
133
     * @psalm-var array<int, string[]|Script[]>
134
     *
135
     * {@see registerJs()}
136
     */
137
    private array $js = [];
138
139
    /**
140
     * @var array the registered JS files.
141
     *
142
     * {@see registerJsFile()}
143
     */
144
    private array $jsFiles = [];
145
146
    /**
147
     * Marks the position of an HTML head section.
148
     */
149 16
    public function head(): void
150
    {
151 16
        echo sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature());
152 16
    }
153
154
    /**
155
     * Marks the beginning of an HTML body section.
156
     */
157 16
    public function beginBody(): void
158
    {
159 16
        echo sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature());
160 16
        $this->eventDispatcher->dispatch(new BodyBegin($this->getViewFile()));
161 16
    }
162
163
    /**
164
     * Marks the ending of an HTML body section.
165
     */
166 16
    public function endBody(): void
167
    {
168 16
        $this->eventDispatcher->dispatch(new BodyEnd($this->getViewFile()));
169 16
        echo sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature());
170 16
    }
171
172
    /**
173
     * Marks the ending of an HTML page.
174
     *
175
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
176
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like
177
     * normal scripts.
178
     */
179 16
    public function endPage($ajaxMode = false): void
180
    {
181 16
        $this->eventDispatcher->dispatch(new PageEnd($this->getViewFile()));
182
183 16
        $content = ob_get_clean();
184
185 16
        echo strtr($content, [
186 16
            sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature()) => $this->renderHeadHtml(),
187 16
            sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature()) => $this->renderBodyBeginHtml(),
188 16
            sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature()) => $this->renderBodyEndHtml($ajaxMode),
189
        ]);
190
191 16
        $this->clear();
192 16
    }
193
194
    /**
195
     * Renders a view in response to an AJAX request.
196
     *
197
     * This method is similar to {@see render()} except that it will surround the view being rendered with the calls of
198
     * {@see beginPage()}, {@see head()}, {@see beginBody()}, {@see endBody()} and {@see endPage()}. By doing so, the
199
     * method is able to inject into the rendering result with JS/CSS scripts and files that are registered with the
200
     * view.
201
     *
202
     * @param string $view the view name. Please refer to {@see render()} on how to specify this parameter.
203
     * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view
204
     * file.
205
     *
206
     * @return string the rendering result
207
     *
208
     * {@see render()}
209
     */
210 2
    public function renderAjax(string $view, array $params = []): string
211
    {
212 2
        $viewFile = $this->findTemplateFile($view);
213
214 2
        ob_start();
215 2
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
216
217 2
        $this->beginPage();
218 2
        $this->head();
219 2
        $this->beginBody();
220 2
        echo $this->renderFile($viewFile, $params);
221 2
        $this->endBody();
222 2
        $this->endPage(true);
223
224 2
        return ob_get_clean();
225
    }
226
227
    /**
228
     * Clears up the registered meta tags, link tags, css/js scripts and files.
229
     */
230 16
    public function clear(): void
231
    {
232 16
        $this->metaTags = [];
233 16
        $this->linkTags = [];
234 16
        $this->css = [];
235 16
        $this->cssFiles = [];
236 16
        $this->js = [];
237 16
        $this->jsFiles = [];
238 16
    }
239
240
    /**
241
     * Registers a meta tag.
242
     *
243
     * For example, a description meta tag can be added like the following:
244
     *
245
     * ```php
246
     * $view->registerMetaTag([
247
     *     'name' => 'description',
248
     *     'content' => 'This website is about funny raccoons.'
249
     * ]);
250
     * ```
251
     *
252
     * will result in the meta tag `<meta name="description" content="This website is about funny raccoons.">`.
253
     *
254
     * @param array $options the HTML attributes for the meta tag.
255
     * @param string $key the key that identifies the meta tag. If two meta tags are registered with the same key, the
256
     * latter will overwrite the former. If this is null, the new meta tag will be appended to the
257
     * existing ones.
258
     */
259 1
    public function registerMetaTag(array $options, string $key = null): void
260
    {
261 1
        if ($key === null) {
262 1
            $this->metaTags[] = Html::meta()->attributes($options)->render();
263
        } else {
264
            $this->metaTags[$key] = Html::meta()->attributes($options)->render();
265
        }
266 1
    }
267
268
    /**
269
     * Registers a link tag.
270
     *
271
     * For example, a link tag for a custom [favicon](http://www.w3.org/2005/10/howto-favicon) can be added like the
272
     * following:
273
     *
274
     * ```php
275
     * $view->registerLinkTag(['rel' => 'icon', 'type' => 'image/png', 'href' => '/myicon.png']);
276
     * ```
277
     *
278
     * which will result in the following HTML: `<link rel="icon" type="image/png" href="/myicon.png">`.
279
     *
280
     * **Note:** To register link tags for CSS stylesheets, use {@see registerCssFile()]} instead, which has more
281
     * options for this kind of link tag.
282
     *
283
     * @param array $options the HTML attributes for the link tag.
284
     * @param string|null $key the key that identifies the link tag. If two link tags are registered with the same
285
     * key, the latter will overwrite the former. If this is null, the new link tag will be appended
286
     * to the existing ones.
287
     */
288 1
    public function registerLinkTag(array $options, ?string $key = null): void
289
    {
290 1
        if ($key === null) {
291 1
            $this->linkTags[] = Html::link()->attributes($options)->render();
292
        } else {
293
            $this->linkTags[$key] = Html::link()->attributes($options)->render();
294
        }
295 1
    }
296
297
    /**
298
     * Registers a CSS code block.
299
     *
300
     * @param string $css the content of the CSS code block to be registered
301
     * @param string|null $key the key that identifies the CSS code block. If null, it will use $css as the key. If two CSS
302
     * code blocks are registered with the same key, the latter will overwrite the former.
303
     */
304 4
    public function registerCss(string $css, int $position = self::DEFAULT_POSITION_CSS_STRING, ?string $key = null): void
305
    {
306 4
        $key = $key ?: md5($css);
307 4
        $this->css[$position][$key] = $css;
308 4
    }
309
310
    /**
311
     * Register a `style` tag
312
     *
313
     * @see registerJs()
314
     */
315 1
    public function registerStyleTag(Style $style, int $position = self::DEFAULT_POSITION_CSS_STRING, ?string $key = null): void
316
    {
317 1
        $key = $key ?: md5($style->render());
318 1
        $this->css[$position][$key] = $style;
319 1
    }
320
321
    /**
322
     * Registers a CSS file.
323
     *
324
     * This method should be used for simple registration of CSS files. If you want to use features of
325
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
326
     * {@see \Yiisoft\Assets\AssetBundle}.
327
     *
328
     * @param string $url the CSS file to be registered.
329
     * @param array $options the HTML attributes for the link tag. Please refer to {@see \Yiisoft\Html\Html::cssFile()}
330
     * for the supported options.
331
     * @param string $key the key that identifies the CSS script file. If null, it will use $url as the key. If two CSS
332
     * files are registered with the same key, the latter will overwrite the former.
333
     */
334 1
    public function registerCssFile(string $url, int $position = self::DEFAULT_POSITION_CSS_FILE, array $options = [], string $key = null): void
335
    {
336 1
        if (!$this->isValidCssPosition($position)) {
337
            throw new InvalidArgumentException('Invalid position of CSS file.');
338
        }
339
340 1
        $this->cssFiles[$position][$key ?: $url] = Html::cssFile($url, $options)->render();
341 1
    }
342
343
    /**
344
     * Registers a JS code block.
345
     *
346
     * @param string $js the JS code block to be registered
347
     * @param int $position the position at which the JS script tag should be inserted in a page.
348
     *
349
     * The possible values are:
350
     *
351
     * - {@see POSITION_HEAD}: in the head section
352
     * - {@see POSITION_BEGIN}: at the beginning of the body section
353
     * - {@see POSITION_END}: at the end of the body section. This is the default value.
354
     * - {@see POSITION_LOAD}: executed when HTML page is completely loaded.
355
     * - {@see POSITION_READY}: executed when HTML document composition is ready.
356
     * @param string $key the key that identifies the JS code block. If null, it will use $js as the key. If two JS code
357
     * blocks are registered with the same key, the latter will overwrite the former.
358
     */
359 5
    public function registerJs(string $js, int $position = self::DEFAULT_POSITION_JS_FILE, ?string $key = null): void
360
    {
361 5
        $key = $key ?: md5($js);
362 5
        $this->js[$position][$key] = $js;
363 5
    }
364
365
    /**
366
     * Register a `script` tag
367
     *
368
     * @see registerJs()
369
     */
370 4
    public function registerScriptTag(Script $script, int $position = self::DEFAULT_POSITION_JS_STRING, ?string $key = null): void
371
    {
372 4
        $key = $key ?: md5($script->render());
373 4
        $this->js[$position][$key] = $script;
374 4
    }
375
376
    /**
377
     * Registers a JS file.
378
     *
379
     * This method should be used for simple registration of JS files. If you want to use features of
380
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
381
     * {@see \Yiisoft\Assets\AssetBundle}.
382
     *
383
     * @param string $url the JS file to be registered.
384
     * @param array $options the HTML attributes for the script tag. The following options are specially handled and
385
     * are not treated as HTML attributes:
386
     *
387
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
388
     *     * {@see POSITION_HEAD}: in the head section
389
     *     * {@see POSITION_BEGIN}: at the beginning of the body section
390
     *     * {@see POSITION_END}: at the end of the body section. This is the default value.
391
     *
392
     * Please refer to {@see \Yiisoft\Html\Html::javaScriptFile()} for other supported options.
393
     * @param string $key the key that identifies the JS script file. If null, it will use $url as the key. If two JS
394
     * files are registered with the same key at the same position, the latter will overwrite the former.
395
     * Note that position option takes precedence, thus files registered with the same key, but different
396
     * position option will not override each other.
397
     */
398 1
    public function registerJsFile(string $url, int $position = self::DEFAULT_POSITION_JS_FILE, array $options = [], string $key = null): void
399
    {
400 1
        if (!$this->isValidJsPosition($position)) {
401
            throw new InvalidArgumentException('Invalid position of JS file.');
402
        }
403
404 1
        $this->jsFiles[$position][$key ?: $url] = Html::javaScriptFile($url, $options)->render();
405 1
    }
406
407
    /**
408
     * Registers a JS code block defining a variable. The name of variable will be used as key, preventing duplicated
409
     * variable names.
410
     *
411
     * @param string $name Name of the variable
412
     * @param array|string $value Value of the variable
413
     * @param int $position the position in a page at which the JavaScript variable should be inserted.
414
     *
415
     * The possible values are:
416
     *
417
     * - {@see POSITION_HEAD}: in the head section. This is the default value.
418
     * - {@see POSITION_BEGIN}: at the beginning of the body section.
419
     * - {@see POSITION_END}: at the end of the body section.
420
     * - {@see POSITION_LOAD}: enclosed within jQuery(window).load().
421
     *   Note that by using this position, the method will automatically register the jQuery js file.
422
     * - {@see POSITION_READY}: enclosed within jQuery(document).ready().
423
     *   Note that by using this position, the method will automatically register the jQuery js file.
424
     */
425 2
    public function registerJsVar(string $name, $value, int $position = self::DEFAULT_POSITION_JS_VARIABLE): void
426
    {
427 2
        $js = sprintf('var %s = %s;', $name, Json::htmlEncode($value));
428 2
        $this->registerJs($js, $position, $name);
429 2
    }
430
431
    /**
432
     * Renders the content to be inserted in the head section.
433
     *
434
     * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files.
435
     *
436
     * @return string the rendered content
437
     */
438 16
    protected function renderHeadHtml(): string
439
    {
440 16
        $lines = [];
441 16
        if (!empty($this->metaTags)) {
442 1
            $lines[] = implode("\n", $this->metaTags);
443
        }
444
445 16
        if (!empty($this->linkTags)) {
446 1
            $lines[] = implode("\n", $this->linkTags);
447
        }
448 16
        if (!empty($this->cssFiles[self::POSITION_HEAD])) {
449 1
            $lines[] = implode("\n", $this->cssFiles[self::POSITION_HEAD]);
450
        }
451 16
        if (!empty($this->css[self::POSITION_HEAD])) {
452 2
            $lines[] = $this->generateCss($this->css[self::POSITION_HEAD]);
453
        }
454 16
        if (!empty($this->jsFiles[self::POSITION_HEAD])) {
455 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_HEAD]);
456
        }
457 16
        if (!empty($this->js[self::POSITION_HEAD])) {
458 3
            $lines[] = $this->generateJs($this->js[self::POSITION_HEAD]);
459
        }
460
461 16
        return empty($lines) ? '' : implode("\n", $lines);
462
    }
463
464
    /**
465
     * Renders the content to be inserted at the beginning of the body section.
466
     *
467
     * The content is rendered using the registered JS code blocks and files.
468
     *
469
     * @return string the rendered content
470
     */
471 16
    protected function renderBodyBeginHtml(): string
472
    {
473 16
        $lines = [];
474 16
        if (!empty($this->cssFiles[self::POSITION_BEGIN])) {
475
            $lines[] = implode("\n", $this->cssFiles[self::POSITION_BEGIN]);
476
        }
477 16
        if (!empty($this->css[self::POSITION_BEGIN])) {
478 2
            $lines[] = $this->generateCss($this->css[self::POSITION_BEGIN]);
479
        }
480 16
        if (!empty($this->jsFiles[self::POSITION_BEGIN])) {
481 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_BEGIN]);
482
        }
483 16
        if (!empty($this->js[self::POSITION_BEGIN])) {
484 1
            $lines[] = $this->generateJs($this->js[self::POSITION_BEGIN]);
485
        }
486
487 16
        return empty($lines) ? '' : implode("\n", $lines);
488
    }
489
490
    /**
491
     * Renders the content to be inserted at the end of the body section.
492
     *
493
     * The content is rendered using the registered JS code blocks and files.
494
     *
495
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
496
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like normal
497
     * scripts.
498
     *
499
     * @return string the rendered content
500
     */
501 16
    protected function renderBodyEndHtml(bool $ajaxMode): string
502
    {
503 16
        $lines = [];
504
505 16
        if (!empty($this->cssFiles[self::POSITION_END])) {
506
            $lines[] = implode("\n", $this->cssFiles[self::POSITION_END]);
507
        }
508 16
        if (!empty($this->css[self::POSITION_END])) {
509 2
            $lines[] = $this->generateCss($this->css[self::POSITION_END]);
510
        }
511 16
        if (!empty($this->jsFiles[self::POSITION_END])) {
512 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_END]);
513
        }
514
515 16
        if ($ajaxMode) {
516 2
            $scripts = array_merge(
517 2
                $this->js[self::POSITION_END] ?? [],
518 2
                $this->js[self::POSITION_READY] ?? [],
519 2
                $this->js[self::POSITION_LOAD] ?? [],
520
            );
521 2
            if (!empty($scripts)) {
522 2
                $lines[] = $this->generateJs($scripts);
523
            }
524
        } else {
525 14
            if (!empty($this->js[self::POSITION_END])) {
526 4
                $lines[] = $this->generateJs($this->js[self::POSITION_END]);
527
            }
528 14
            if (!empty($this->js[self::POSITION_READY])) {
529
                $js = "document.addEventListener('DOMContentLoaded', function(event) {\n" .
530 1
                    $this->generateJsWithoutTag($this->js[self::POSITION_READY]) .
531 1
                    "\n});";
532 1
                $lines[] = Html::script($js)->render();
533
            }
534 14
            if (!empty($this->js[self::POSITION_LOAD])) {
535
                $js = "window.addEventListener('load', function (event) {\n" .
536
                    $this->generateJsWithoutTag($this->js[self::POSITION_LOAD]) .
537
                    "\n});";
538
                $lines[] = Html::script($js)->render();
539
            }
540
        }
541
542 16
        return empty($lines) ? '' : implode("\n", $lines);
543
    }
544
545
    /**
546
     * Get title in views.
547
     *
548
     * in Layout:
549
     *
550
     * ```php
551
     * <title><?= Html::encode($this->getTitle()) ?></title>
552
     * ```
553
     *
554
     * in Views:
555
     *
556
     * ```php
557
     * $this->setTitle('Web Application - Yii 3.0.');
558
     * ```
559
     *
560
     * @return string
561
     */
562
    public function getTitle(): string
563
    {
564
        return $this->title;
565
    }
566
567
    /**
568
     * It processes the CSS configuration generated by the asset manager and converts it into HTML code.
569
     *
570
     * @param array $cssFiles
571
     */
572
    public function setCssFiles(array $cssFiles): void
573
    {
574
        foreach ($cssFiles as $key => $value) {
575
            $this->registerCssFileByConfig(
576
                is_string($key) ? $key : null,
577
                is_array($value) ? $value : [$value],
578
            );
579
        }
580
    }
581
582 1
    public function setCssStrings(array $cssStrings): void
583
    {
584
        /** @var mixed $value */
585 1
        foreach ($cssStrings as $key => $value) {
586 1
            $this->registerCssStringByConfig(
587 1
                is_string($key) ? $key : null,
588 1
                is_array($value) ? $value : [$value, self::DEFAULT_POSITION_CSS_STRING]
589
            );
590
        }
591 1
    }
592
593
    /**
594
     * It processes the JS configuration generated by the asset manager and converts it into HTML code.
595
     *
596
     * @param array $jsFiles
597
     */
598
    public function setJsFiles(array $jsFiles): void
599
    {
600
        foreach ($jsFiles as $key => $value) {
601
            $this->registerJsFileByConfig(
602
                is_string($key) ? $key : null,
603
                is_array($value) ? $value : [$value],
604
            );
605
        }
606
    }
607
608
    /**
609
     * It processes the JS strings generated by the asset manager.
610
     *
611
     * @param array $jsStrings
612
     *
613
     * @throws InvalidArgumentException
614
     */
615 7
    public function setJsStrings(array $jsStrings): void
616
    {
617
        /** @var mixed $value */
618 7
        foreach ($jsStrings as $key => $value) {
619 7
            $this->registerJsStringByConfig(
620 7
                is_string($key) ? $key : null,
621 7
                is_array($value) ? $value : [$value, self::DEFAULT_POSITION_JS_STRING]
622
            );
623
        }
624 1
    }
625
626
    /**
627
     * It processes the JS variables generated by the asset manager and converts it into JS code.
628
     *
629
     * @param array $jsVars
630
     *
631
     * @throws InvalidArgumentException
632
     */
633 5
    public function setJsVars(array $jsVars): void
634
    {
635 5
        foreach ($jsVars as $key => $value) {
636 5
            if (is_string($key)) {
637 1
                $this->registerJsVar($key, $value, self::DEFAULT_POSITION_JS_VARIABLE);
638
            } else {
639 5
                $this->registerJsVarByConfig($value);
640
            }
641
        }
642 1
    }
643
644
    /**
645
     * Set title in views.
646
     *
647
     * {@see getTitle()}
648
     *
649
     * @param string $value
650
     */
651
    public function setTitle(string $value): void
652
    {
653
        $this->title = $value;
654
    }
655
656
    /**
657
     * @throws InvalidArgumentException
658
     */
659
    private function registerCssFileByConfig(?string $key, array $config): void
660
    {
661
        if (!array_key_exists(0, $config)) {
662
            throw new InvalidArgumentException('Do not set CSS file.');
663
        }
664
        $file = $config[0];
665
666
        if (!is_string($file)) {
667
            throw new InvalidArgumentException(
668
                sprintf(
669
                    'CSS file should be string. Got %s.',
670
                    $this->getType($file),
671
                )
672
            );
673
        }
674
675
        $position = $config[1] ?? self::DEFAULT_POSITION_CSS_FILE;
676
677
        unset($config[0], $config[1]);
678
        $this->registerCssFile($file, $position, $config, $key);
679
    }
680
681
    /**
682
     * @throws InvalidArgumentException
683
     */
684 1
    private function registerCssStringByConfig(?string $key, array $config): void
685
    {
686 1
        if (!array_key_exists(0, $config)) {
687
            throw new InvalidArgumentException('Do not set CSS string.');
688
        }
689 1
        $css = $config[0];
690
691 1
        if (!is_string($css) && !($css instanceof Style)) {
692
            throw new InvalidArgumentException(
693
                sprintf(
694
                    'CSS string should be string or instance of \\' . Style::class . '. Got %s.',
695
                    $this->getType($css),
696
                )
697
            );
698
        }
699
700 1
        $position = $config[1] ?? self::DEFAULT_POSITION_CSS_STRING;
701 1
        if (!$this->isValidCssPosition($position)) {
702
            throw new InvalidArgumentException('Invalid position of CSS strings.');
703
        }
704
705 1
        unset($config[0], $config[1]);
706 1
        if ($config !== []) {
707 1
            $css = ($css instanceof Style ? $css : Html::style($css))->attributes($config);
708
        }
709
710 1
        is_string($css)
711 1
            ? $this->registerCss($css, $position, $key)
712 1
            : $this->registerStyleTag($css, $position, $key);
713 1
    }
714
715
    /**
716
     * @throws InvalidArgumentException
717
     */
718
    private function registerJsFileByConfig(?string $key, array $config): void
719
    {
720
        if (!array_key_exists(0, $config)) {
721
            throw new InvalidArgumentException('Do not set JS file.');
722
        }
723
        $file = $config[0];
724
725
        if (!is_string($file)) {
726
            throw new InvalidArgumentException(
727
                sprintf(
728
                    'JS file should be string. Got %s.',
729
                    $this->getType($file),
730
                )
731
            );
732
        }
733
734
        $position = $config[1] ?? self::DEFAULT_POSITION_JS_FILE;
735
736
        unset($config[0], $config[1]);
737
        $this->registerJsFile($file, $position, $config, $key);
738
    }
739
740
    /**
741
     * @throws InvalidArgumentException
742
     */
743 7
    private function registerJsStringByConfig(?string $key, array $config): void
744
    {
745 7
        if (!array_key_exists(0, $config)) {
746 2
            throw new InvalidArgumentException('Do not set JS string.');
747
        }
748 5
        $js = $config[0];
749
750 5
        if (!is_string($js) && !($js instanceof Script)) {
751 2
            throw new InvalidArgumentException(
752 2
                sprintf(
753 2
                    'JS string should be string or instance of \\' . Script::class . '. Got %s.',
754 2
                    $this->getType($js),
755
                )
756
            );
757
        }
758
759 3
        $position = $config[1] ?? self::DEFAULT_POSITION_JS_STRING;
760 3
        if (!$this->isValidJsPosition($position)) {
761 2
            throw new InvalidArgumentException('Invalid position of JS strings.');
762
        }
763
764 1
        unset($config[0], $config[1]);
765 1
        if ($config !== []) {
766
            $js = ($js instanceof Script ? $js : Html::script($js))->attributes($config);
767
        }
768
769 1
        is_string($js)
770 1
            ? $this->registerJs($js, $position, $key)
771 1
            : $this->registerScriptTag($js, $position, $key);
772 1
    }
773
774
    /**
775
     * @throws InvalidArgumentException
776
     */
777 5
    private function registerJsVarByConfig(array $config): void
778
    {
779 5
        if (!array_key_exists(0, $config)) {
780 1
            throw new InvalidArgumentException('Do not set JS variable name.');
781
        }
782 4
        $key = $config[0];
783
784 4
        if (!is_string($key)) {
785 1
            throw new InvalidArgumentException(
786 1
                sprintf(
787 1
                    'JS variable name should be string. Got %s.',
788 1
                    $this->getType($key),
789
                )
790
            );
791
        }
792
793 3
        if (!array_key_exists(1, $config)) {
794 1
            throw new InvalidArgumentException('Do not set JS variable value.');
795
        }
796
        /** @var mixed */
797 2
        $value = $config[1];
798
799 2
        $position = $config[2] ?? self::DEFAULT_POSITION_JS_VARIABLE;
800 2
        if (!$this->isValidJsPosition($position)) {
801 1
            throw new InvalidArgumentException('Invalid position of JS variable.');
802
        }
803
804 1
        $this->registerJsVar($key, $value, $position);
805 1
    }
806
807
    /**
808
     * @param string[]|Style[] $items
809
     */
810 4
    private function generateCss(array $items): string
811
    {
812 4
        $lines = [];
813
814 4
        $css = [];
815 4
        foreach ($items as $item) {
816 4
            if ($item instanceof Style) {
817 1
                if ($css !== []) {
818 1
                    $lines[] = Html::style(implode("\n", $css))->render();
819 1
                    $css = [];
820
                }
821 1
                $lines[] = $item->render();
822
            } else {
823 4
                $css[] = $item;
824
            }
825
        }
826 4
        if ($css !== []) {
827 4
            $lines[] = Html::style(implode("\n", $css))->render();
828
        }
829
830 4
        return implode("\n", $lines);
831
    }
832
833
    /**
834
     * @param Script[]|string[] $items
835
     */
836 6
    private function generateJs(array $items): string
837
    {
838 6
        $lines = [];
839
840 6
        $js = [];
841 6
        foreach ($items as $item) {
842 6
            if ($item instanceof Script) {
843 4
                if ($js !== []) {
844 3
                    $lines[] = Html::script(implode("\n", $js))->render();
845 3
                    $js = [];
846
                }
847 4
                $lines[] = $item->render();
848
            } else {
849 5
                $js[] = $item;
850
            }
851
        }
852 6
        if ($js !== []) {
853 4
            $lines[] = Html::script(implode("\n", $js))->render();
854
        }
855
856 6
        return implode("\n", $lines);
857
    }
858
859
    /**
860
     * @param Script[]|string[] $items
861
     */
862 1
    private function generateJsWithoutTag(array $items): string
863
    {
864 1
        $js = [];
865 1
        foreach ($items as $item) {
866 1
            $js[] = $item instanceof Script ? $item->getContent() : $item;
867
        }
868 1
        return implode("\n", $js);
869
    }
870
871
    /**
872
     * @param mixed $position
873
     *
874
     * @psalm-assert =int $position
875
     */
876 2
    private function isValidCssPosition($position): bool
877
    {
878 2
        return in_array(
879 2
            $position,
880
            [
881 2
                self::POSITION_HEAD,
882 2
                self::POSITION_BEGIN,
883 2
                self::POSITION_END,
884
            ],
885 2
            true,
886
        );
887
    }
888
889
    /**
890
     * @param mixed $position
891
     *
892
     * @psalm-assert =int $position
893
     */
894 6
    private function isValidJsPosition($position): bool
895
    {
896 6
        return in_array(
897 6
            $position,
898
            [
899 6
                self::POSITION_HEAD,
900 6
                self::POSITION_BEGIN,
901 6
                self::POSITION_END,
902 6
                self::POSITION_READY,
903 6
                self::POSITION_LOAD,
904
            ],
905 6
            true,
906
        );
907
    }
908
909
    /**
910
     * @param mixed $value
911
     */
912 3
    private function getType($value): string
913
    {
914 3
        return is_object($value) ? get_class($value) : gettype($value);
915
    }
916
}
917