Passed
Pull Request — master (#154)
by Sergei
02:22
created

WebView::addJsFiles()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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