Passed
Push — master ( 2af18a...ebd8c1 )
by Alexander
03:43
created

WebView::createAfterRenderEvent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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