Passed
Push — master ( 808cac...64bfdf )
by Evgeniy
02:17
created

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