Passed
Pull Request — master (#143)
by Alexander
02:07
created

WebView::clear()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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