Passed
Pull Request — master (#195)
by Sergei
02:31
created

WebView::clear()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 8
c 1
b 0
f 1
dl 0
loc 10
ccs 9
cts 9
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1

1 Method

Rating   Name   Duplication   Size   Complexity  
A WebView::registerMeta() 0 4 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\View;
6
7
use InvalidArgumentException;
8
use Psr\EventDispatcher\EventDispatcherInterface;
9
use Psr\EventDispatcher\StoppableEventInterface;
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\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
use Yiisoft\View\State\WebViewState;
24
25
use function array_merge;
26
use function implode;
27
use function ob_get_clean;
28
use function ob_implicit_flush;
29
use function ob_start;
30
use function sprintf;
31
use function strtr;
32
33
/**
34
 * WebView represents an instance of a view for use in a WEB environment.
35
 *
36
 * WebView provides a set of methods (e.g. {@see WebView::render()}) for rendering purpose.
37
 */
38
final class WebView implements ViewInterface
39
{
40
    use ViewTrait;
41
42
    private WebViewState $state;
43
44
    /**
45
     * This means the location is in the head section.
46
     */
47
    public const POSITION_HEAD = 1;
48
49
    /**
50
     * This means the location is at the beginning of the body section.
51
     */
52
    public const POSITION_BEGIN = 2;
53
54
    /**
55
     * This means the location is at the end of the body section.
56
     */
57
    public const POSITION_END = 3;
58
59
    /**
60
     * This means the JavaScript code block will be executed when HTML document composition is ready.
61
     */
62
    public const POSITION_READY = 4;
63
64
    /**
65
     * This means the JavaScript code block will be executed when HTML page is completely loaded.
66
     */
67
    public const POSITION_LOAD = 5;
68
69
    /**
70
     * This is internally used as the placeholder for receiving the content registered for the head section.
71
     */
72
    private const PLACEHOLDER_HEAD = '<![CDATA[YII-BLOCK-HEAD-%s]]>';
73
74
    /**
75
     * This is internally used as the placeholder for receiving the content registered for the beginning of the body
76
     * section.
77
     */
78
    private const PLACEHOLDER_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN-%s]]>';
79
80
    /**
81
     * This is internally used as the placeholder for receiving the content registered for the end of the body section.
82
     */
83
    private const PLACEHOLDER_BODY_END = '<![CDATA[YII-BLOCK-BODY-END-%s]]>';
84
85
    /**
86
     * @param string $basePath The full path to the base directory of views.
87
     * @param EventDispatcherInterface $eventDispatcher The event dispatcher instance.
88
     */
89 82
    public function __construct(string $basePath, EventDispatcherInterface $eventDispatcher)
90
    {
91 82
        $this->basePath = $basePath;
92 82
        $this->state = new WebViewState();
93 82
        $this->eventDispatcher = $eventDispatcher;
94 82
        $this->setPlaceholderSalt(__DIR__);
95 82
    }
96
97
    /**
98
     * Marks the position of an HTML head section.
99
     */
100 47
    public function head(): void
101
    {
102 47
        echo sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature());
103 47
        $this->eventDispatcher->dispatch(new Head($this));
104 47
    }
105
106
    /**
107
     * Marks the beginning of an HTML body section.
108
     */
109 47
    public function beginBody(): void
110
    {
111 47
        echo sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature());
112 47
        $this->eventDispatcher->dispatch(new BodyBegin($this));
113 47
    }
114
115
    /**
116
     * Marks the ending of an HTML body section.
117
     */
118 47
    public function endBody(): void
119
    {
120 47
        $this->eventDispatcher->dispatch(new BodyEnd($this));
121 47
        echo sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature());
122 47
    }
123
124
    /**
125
     * Marks the beginning of an HTML page.
126
     */
127 47
    public function beginPage(): void
128
    {
129 47
        ob_start();
130
        /** @psalm-suppress InvalidArgument */
131 47
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
132
133 47
        $this->eventDispatcher->dispatch(new PageBegin($this));
134 47
    }
135
136
    /**
137
     * Marks the ending of an HTML page.
138
     *
139
     * @param bool $ajaxMode Whether the view is rendering in AJAX mode. If true, the JS scripts registered at
140
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like
141
     * normal scripts.
142
     */
143 47
    public function endPage(bool $ajaxMode = false): void
144
    {
145 47
        $this->eventDispatcher->dispatch(new PageEnd($this));
146
147 47
        $content = ob_get_clean();
148
149 47
        echo strtr($content, [
150 47
            sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature()) => $this->renderHeadHtml(),
151 47
            sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature()) => $this->renderBodyBeginHtml(),
152 47
            sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature()) => $this->renderBodyEndHtml($ajaxMode),
153
        ]);
154
155 47
        $this->clear();
156 47
    }
157
158
    /**
159
     * Renders a view in response to an AJAX request.
160
     *
161
     * This method is similar to {@see render()} except that it will surround the view being rendered with the calls of
162
     * {@see beginPage()}, {@see head()}, {@see beginBody()}, {@see endBody()} and {@see endPage()}. By doing so, the
163
     * method is able to inject into the rendering result with JS/CSS scripts and files that are registered with the
164
     * view.
165
     *
166
     * @param string $view The view name. Please refer to {@see render()} on how to specify this parameter.
167
     * @param array $parameters The parameters (name-value pairs) that will be extracted and made available in the view
168
     * file.
169
     *
170
     * @return string The rendering result
171
     *
172
     * {@see render()}
173
     */
174 2
    public function renderAjax(string $view, array $parameters = []): string
175
    {
176 2
        $viewFile = $this->findTemplateFile($view);
177
178 2
        ob_start();
179
        /** @psalm-suppress InvalidArgument */
180 2
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
181
182 2
        $this->beginPage();
183 2
        $this->head();
184 2
        $this->beginBody();
185 2
        echo $this->renderFile($viewFile, $parameters);
186 2
        $this->endBody();
187 2
        $this->endPage(true);
188
189 2
        return ob_get_clean();
190
    }
191
192
    /**
193
     * Renders a string in response to an AJAX request.
194
     *
195
     * @param string $string The string.
196
     *
197
     * @return string The rendering result.
198
     */
199 1
    public function renderAjaxString(string $string): string
200
    {
201 1
        ob_start();
202
        /** @psalm-suppress InvalidArgument */
203 1
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
204
205 1
        $this->beginPage();
206 1
        $this->head();
207 1
        $this->beginBody();
208 1
        echo $string;
209 1
        $this->endBody();
210 1
        $this->endPage(true);
211
212 1
        return ob_get_clean();
213
    }
214
215
    /**
216
     * Get title in views.
217
     *
218
     * in Layout:
219
     *
220
     * ```php
221
     * <title><?= Html::encode($this->getTitle()) ?></title>
222
     * ```
223
     *
224
     * in Views:
225
     *
226
     * ```php
227
     * $this->setTitle('Web Application - Yii 3.0.');
228
     * ```
229
     *
230
     * @return string
231
     */
232 2
    public function getTitle(): string
233
    {
234 2
        return $this->state->getTitle();
235
    }
236
237
    /**
238
     * Set title in views.
239
     *
240
     * {@see getTitle()}
241
     *
242
     * @param string $value
243
     *
244
     * @return static
245
     */
246 3
    public function setTitle(string $value): self
247
    {
248 3
        $this->state->setTitle($value);
249 3
        return $this;
250
    }
251
252
    /**
253
     * Registers a meta tag.
254
     *
255
     * For example, a description meta tag can be added like the following:
256
     *
257
     * ```php
258
     * $view->registerMeta([
259
     *     'name' => 'description',
260
     *     'content' => 'This website is about funny raccoons.'
261
     * ]);
262
     * ```
263
     *
264
     * will result in the meta tag `<meta name="description" content="This website is about funny raccoons.">`.
265
     *
266
     * @param array $attributes The HTML attributes for the meta tag.
267
     * @param string|null $key The key that identifies the meta tag. If two meta tags are registered with the same key,
268
     * the latter will overwrite the former. If this is null, the new meta tag will be appended to the existing ones.
269
     *
270
     * @return static
271
     */
272 2
    public function registerMeta(array $attributes, ?string $key = null): self
273
    {
274 2
        $this->state->registerMeta($attributes, $key);
275 2
        return $this;
276
    }
277
278
    /**
279
     * Registers a {@see Meta} tag.
280
     *
281
     * @return static
282
     *
283
     * @see registerMeta()
284
     */
285 4
    public function registerMetaTag(Meta $meta, ?string $key = null): self
286
    {
287 4
        $this->state->registerMetaTag($meta, $key);
288 4
        return $this;
289
    }
290
291
    /**
292
     * Registers a link tag.
293
     *
294
     * For example, a link tag for a custom [favicon](http://www.w3.org/2005/10/howto-favicon) can be added like the
295
     * following:
296
     *
297
     * ```php
298
     * $view->registerLink(['rel' => 'icon', 'type' => 'image/png', 'href' => '/myicon.png']);
299
     * ```
300
     *
301
     * which will result in the following HTML: `<link rel="icon" type="image/png" href="/myicon.png">`.
302
     *
303
     * **Note:** To register link tags for CSS stylesheets, use {@see registerCssFile()]} instead, which has more
304
     * options for this kind of link tag.
305
     *
306
     * @param array $attributes The HTML attributes for the link tag.
307
     * @param int $position The position at which the link tag should be inserted in a page.
308
     * @param string|null $key The key that identifies the link tag. If two link tags are registered with the same
309
     * key, the latter will overwrite the former. If this is null, the new link tag will be appended
310
     * to the existing ones.
311
     *
312
     * @return static
313
     */
314 4
    public function registerLink(array $attributes, int $position = self::POSITION_HEAD, ?string $key = null): self
315
    {
316 4
        $this->state->registerLink($attributes, $position, $key);
317 4
        return $this;
318
    }
319
320
    /**
321
     * Registers a {@see Link} tag.
322
     *
323
     * @return static
324
     *
325
     * @see registerLink()
326
     */
327 6
    public function registerLinkTag(Link $link, int $position = self::POSITION_HEAD, ?string $key = null): self
328
    {
329 6
        $this->state->registerLinkTag($link, $position, $key);
330 6
        return $this;
331
    }
332
333
    /**
334
     * Registers a CSS code block.
335
     *
336
     * @param string $css The content of the CSS code block to be registered.
337
     * @param string|null $key The key that identifies the CSS code block. If null, it will use $css as the key.
338
     * If two CSS code blocks are registered with the same key, the latter will overwrite the former.
339
     * @param array $attributes The HTML attributes for the {@see Style} tag.
340
     *
341
     * @return static
342
     */
343 6
    public function registerCss(
344
        string $css,
345
        int $position = self::POSITION_HEAD,
346
        array $attributes = [],
347
        ?string $key = null
348
    ): self {
349 6
        $this->state->registerCss($css, $position, $attributes, $key);
350 6
        return $this;
351
    }
352
353
    /**
354
     * Registers a CSS code block from file.
355
     *
356
     * @param string $path The path or URL to CSS file.
357
     *
358
     * @return static
359
     *
360
     * @see registerCss()
361
     */
362 3
    public function registerCssFromFile(
363
        string $path,
364
        int $position = self::POSITION_HEAD,
365
        array $attributes = [],
366
        ?string $key = null
367
    ): self {
368 3
        $this->state->registerCssFromFile($path, $position, $attributes, $key);
369 2
        return $this;
370
    }
371
372
    /**
373
     * Register a {@see Style} tag.
374
     *
375
     * @return static
376
     *
377
     * @see registerJs()
378
     */
379 2
    public function registerStyleTag(Style $style, int $position = self::POSITION_HEAD, ?string $key = null): self
380
    {
381 2
        $this->state->registerStyleTag($style, $position, $key);
382 2
        return $this;
383
    }
384
385
    /**
386
     * Registers a CSS file.
387
     *
388
     * This method should be used for simple registration of CSS files. If you want to use features of
389
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
390
     * {@see \Yiisoft\Assets\AssetBundle}.
391
     *
392
     * @param string $url The CSS file to be registered.
393
     * @param array $options the HTML attributes for the link tag. Please refer to {@see \Yiisoft\Html\Html::cssFile()}
394
     * for the supported options.
395
     * @param string|null $key The key that identifies the CSS script file. If null, it will use $url as the key.
396
     * If two CSS files are registered with the same key, the latter will overwrite the former.
397
     *
398
     * @return static
399
     */
400 12
    public function registerCssFile(
401
        string $url,
402
        int $position = self::POSITION_HEAD,
403
        array $options = [],
404
        string $key = null
405
    ): self {
406 12
        $this->state->registerCssFile($url, $position, $options, $key);
407 11
        return $this;
408
    }
409
410
    /**
411
     * It processes the CSS configuration generated by the asset manager and converts it into HTML code.
412
     *
413
     * @param array $cssFiles
414
     *
415
     * @return static
416
     */
417 5
    public function addCssFiles(array $cssFiles): self
418
    {
419 5
        $this->state->addCssFiles($cssFiles);
420 2
        return $this;
421
    }
422
423
    /**
424
     * It processes the CSS strings generated by the asset manager.
425
     *
426
     * @param array $cssStrings
427
     *
428
     * @return static
429
     */
430 8
    public function addCssStrings(array $cssStrings): self
431
    {
432 8
        $this->state->addCssStrings($cssStrings);
433 2
        return $this;
434
    }
435
436
    /**
437
     * Registers a JS code block.
438
     *
439
     * @param string $js the JS code block to be registered
440
     * @param int $position the position at which the JS script tag should be inserted in a page.
441
     *
442
     * The possible values are:
443
     *
444
     * - {@see POSITION_HEAD}: in the head section
445
     * - {@see POSITION_BEGIN}: at the beginning of the body section
446
     * - {@see POSITION_END}: at the end of the body section. This is the default value.
447
     * - {@see POSITION_LOAD}: executed when HTML page is completely loaded.
448
     * - {@see POSITION_READY}: executed when HTML document composition is ready.
449
     * @param string|null $key The key that identifies the JS code block. If null, it will use $js as the key.
450
     * If two JS code blocks are registered with the same key, the latter will overwrite the former.
451
     *
452
     * @return static
453
     */
454 4
    public function registerJs(string $js, int $position = self::POSITION_END, ?string $key = null): self
455
    {
456 4
        $this->state->registerJs($js, $position, $key);
457 4
        return $this;
458
    }
459
460
    /**
461
     * Register a `script` tag
462
     *
463
     * @return static
464
     *
465
     * @see registerJs()
466
     */
467 4
    public function registerScriptTag(Script $script, int $position = self::POSITION_END, ?string $key = null): self
468
    {
469 4
        $this->state->registerScriptTag($script, $position, $key);
470 4
        return $this;
471
    }
472
473
    /**
474
     * Registers a JS file.
475
     *
476
     * This method should be used for simple registration of JS files. If you want to use features of
477
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
478
     * {@see \Yiisoft\Assets\AssetBundle}.
479
     *
480
     * @param string $url The JS file to be registered.
481
     * @param array $options The HTML attributes for the script tag. The following options are specially handled and
482
     * are not treated as HTML attributes:
483
     *
484
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
485
     *     * {@see POSITION_HEAD}: in the head section
486
     *     * {@see POSITION_BEGIN}: at the beginning of the body section
487
     *     * {@see POSITION_END}: at the end of the body section. This is the default value.
488
     *
489
     * Please refer to {@see \Yiisoft\Html\Html::javaScriptFile()} for other supported options.
490
     * @param string|null $key The key that identifies the JS script file. If null, it will use $url as the key.
491
     * If two JS files are registered with the same key at the same position, the latter will overwrite the former.
492
     * Note that position option takes precedence, thus files registered with the same key, but different
493
     * position option will not override each other.
494
     *
495
     * @return static
496
     */
497 12
    public function registerJsFile(
498
        string $url,
499
        int $position = self::POSITION_END,
500
        array $options = [],
501
        string $key = null
502
    ): self {
503 12
        $this->state->registerJsFile($url, $position, $options, $key);
504 11
        return $this;
505
    }
506
507
    /**
508
     * Registers a JS code block defining a variable. The name of variable will be used as key, preventing duplicated
509
     * variable names.
510
     *
511
     * @param string $name Name of the variable
512
     * @param mixed $value Value of the variable
513
     * @param int $position The position in a page at which the JavaScript variable should be inserted.
514
     *
515
     * The possible values are:
516
     *
517
     * - {@see POSITION_HEAD}: in the head section. This is the default value.
518
     * - {@see POSITION_BEGIN}: at the beginning of the body section.
519
     * - {@see POSITION_END}: at the end of the body section.
520
     * - {@see POSITION_LOAD}: enclosed within jQuery(window).load().
521
     *   Note that by using this position, the method will automatically register the jQuery js file.
522
     * - {@see POSITION_READY}: enclosed within jQuery(document).ready().
523
     *   Note that by using this position, the method will automatically register the jQuery js file.
524
     *
525
     * @return static
526
     */
527 3
    public function registerJsVar(string $name, $value, int $position = self::POSITION_HEAD): self
528
    {
529 3
        $this->state->registerJsVar($name, $value, $position);
530 3
        return $this;
531
    }
532
533
    /**
534
     * It processes the JS configuration generated by the asset manager and converts it into HTML code.
535
     *
536
     * @param array $jsFiles
537
     *
538
     * @return static
539
     */
540 5
    public function addJsFiles(array $jsFiles): self
541
    {
542 5
        $this->state->addJsFiles($jsFiles);
543 2
        return $this;
544
    }
545
546
    /**
547
     * It processes the JS strings generated by the asset manager.
548
     *
549
     * @param array $jsStrings
550
     *
551
     * @return static
552
     *
553
     * @throws InvalidArgumentException
554
     */
555 8
    public function addJsStrings(array $jsStrings): self
556
    {
557 8
        $this->state->addJsStrings($jsStrings);
558 2
        return $this;
559
    }
560
561
    /**
562
     * It processes the JS variables generated by the asset manager and converts it into JS code.
563
     *
564
     * @param array $jsVars
565
     *
566
     * @return static
567
     *
568
     * @throws InvalidArgumentException
569
     */
570 6
    public function addJsVars(array $jsVars): self
571
    {
572 6
        $this->state->addJsVars($jsVars);
573 2
        return $this;
574
    }
575
576 46
    protected function createBeforeRenderEvent(string $viewFile, array $parameters): StoppableEventInterface
577
    {
578 46
        return new BeforeRender($this, $viewFile, $parameters);
579
    }
580
581 46
    protected function createAfterRenderEvent(
582
        string $viewFile,
583
        array $parameters,
584
        string $result
585
    ): AfterRenderEventInterface {
586 46
        return new AfterRender($this, $viewFile, $parameters, $result);
587
    }
588
589
    /**
590
     * Renders the content to be inserted in the head section.
591
     *
592
     * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files.
593
     *
594
     * @return string The rendered content
595
     */
596 47
    private function renderHeadHtml(): string
597
    {
598 47
        $lines = [];
599
600 47
        if (!empty($this->state->getMetaTags())) {
601 3
            $lines[] = implode("\n", $this->state->getMetaTags());
602
        }
603 47
        if (!empty($this->state->getLinkTags()[self::POSITION_HEAD])) {
604 3
            $lines[] = implode("\n", $this->state->getLinkTags()[self::POSITION_HEAD]);
605
        }
606 47
        if (!empty($this->state->getCssFiles()[self::POSITION_HEAD])) {
607 8
            $lines[] = implode("\n", $this->state->getCssFiles()[self::POSITION_HEAD]);
608
        }
609 47
        if (!empty($this->state->getCss()[self::POSITION_HEAD])) {
610 5
            $lines[] = $this->generateCss($this->state->getCss()[self::POSITION_HEAD]);
611
        }
612 47
        if (!empty($this->state->getJsFiles()[self::POSITION_HEAD])) {
613 1
            $lines[] = implode("\n", $this->state->getJsFiles()[self::POSITION_HEAD]);
614
        }
615 47
        if (!empty($this->state->getJs()[self::POSITION_HEAD])) {
616 4
            $lines[] = $this->generateJs($this->state->getJs()[self::POSITION_HEAD]);
617
        }
618
619 47
        return empty($lines) ? '' : implode("\n", $lines);
620
    }
621
622
    /**
623
     * Renders the content to be inserted at the beginning of the body section.
624
     *
625
     * The content is rendered using the registered JS code blocks and files.
626
     *
627
     * @return string The rendered content.
628
     */
629 47
    private function renderBodyBeginHtml(): string
630
    {
631 47
        $lines = [];
632
633 47
        if (!empty($this->state->getLinkTags()[self::POSITION_BEGIN])) {
634 2
            $lines[] = implode("\n", $this->state->getLinkTags()[self::POSITION_BEGIN]);
635
        }
636 47
        if (!empty($this->state->getCssFiles()[self::POSITION_BEGIN])) {
637 2
            $lines[] = implode("\n", $this->state->getCssFiles()[self::POSITION_BEGIN]);
638
        }
639 47
        if (!empty($this->state->getCss()[self::POSITION_BEGIN])) {
640 2
            $lines[] = $this->generateCss($this->state->getCss()[self::POSITION_BEGIN]);
641
        }
642 47
        if (!empty($this->state->getJsFiles()[self::POSITION_BEGIN])) {
643 2
            $lines[] = implode("\n", $this->state->getJsFiles()[self::POSITION_BEGIN]);
644
        }
645 47
        if (!empty($this->state->getJs()[self::POSITION_BEGIN])) {
646 1
            $lines[] = $this->generateJs($this->state->getJs()[self::POSITION_BEGIN]);
647
        }
648
649 47
        return empty($lines) ? '' : implode("\n", $lines);
650
    }
651
652
    /**
653
     * Renders the content to be inserted at the end of the body section.
654
     *
655
     * The content is rendered using the registered JS code blocks and files.
656
     *
657
     * @param bool $ajaxMode Whether the view is rendering in AJAX mode. If true, the JS scripts registered at
658
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like normal
659
     * scripts.
660
     *
661
     * @return string The rendered content.
662
     */
663 47
    private function renderBodyEndHtml(bool $ajaxMode): string
664
    {
665 47
        $lines = [];
666
667 47
        if (!empty($this->state->getLinkTags()[self::POSITION_END])) {
668 2
            $lines[] = implode("\n", $this->state->getLinkTags()[self::POSITION_END]);
669
        }
670 47
        if (!empty($this->state->getCssFiles()[self::POSITION_END])) {
671 1
            $lines[] = implode("\n", $this->state->getCssFiles()[self::POSITION_END]);
672
        }
673 47
        if (!empty($this->state->getCss()[self::POSITION_END])) {
674 2
            $lines[] = $this->generateCss($this->state->getCss()[self::POSITION_END]);
675
        }
676 47
        if (!empty($this->state->getJsFiles()[self::POSITION_END])) {
677 8
            $lines[] = implode("\n", $this->state->getJsFiles()[self::POSITION_END]);
678
        }
679
680 47
        if ($ajaxMode) {
681 3
            $scripts = array_merge(
682 3
                $this->state->getJs()[self::POSITION_END] ?? [],
683 3
                $this->state->getJs()[self::POSITION_READY] ?? [],
684 3
                $this->state->getJs()[self::POSITION_LOAD] ?? [],
685
            );
686 3
            if (!empty($scripts)) {
687 3
                $lines[] = $this->generateJs($scripts);
688
            }
689
        } else {
690 44
            if (!empty($this->state->getJs()[self::POSITION_END])) {
691 4
                $lines[] = $this->generateJs($this->state->getJs()[self::POSITION_END]);
692
            }
693 44
            if (!empty($this->state->getJs()[self::POSITION_READY])) {
694
                $js = "document.addEventListener('DOMContentLoaded', function(event) {\n" .
695 1
                    $this->generateJsWithoutTag($this->state->getJs()[self::POSITION_READY]) .
696 1
                    "\n});";
697 1
                $lines[] = Html::script($js)->render();
698
            }
699 44
            if (!empty($this->state->getJs()[self::POSITION_LOAD])) {
700
                $js = "window.addEventListener('load', function(event) {\n" .
701 1
                    $this->generateJsWithoutTag($this->state->getJs()[self::POSITION_LOAD]) .
702 1
                    "\n});";
703 1
                $lines[] = Html::script($js)->render();
704
            }
705
        }
706
707 47
        return empty($lines) ? '' : implode("\n", $lines);
708
    }
709
710
    /**
711
     * @param string[]|Style[] $items
712
     */
713 7
    private function generateCss(array $items): string
714
    {
715 7
        $lines = [];
716
717 7
        $css = [];
718 7
        foreach ($items as $item) {
719 7
            if ($item instanceof Style) {
720 4
                if ($css !== []) {
721 1
                    $lines[] = Html::style(implode("\n", $css))->render();
722 1
                    $css = [];
723
                }
724 4
                $lines[] = $item->render();
725
            } else {
726 4
                $css[] = $item;
727
            }
728
        }
729 7
        if ($css !== []) {
730 4
            $lines[] = Html::style(implode("\n", $css))->render();
731
        }
732
733 7
        return implode("\n", $lines);
734
    }
735
736
    /**
737
     * @param Script[]|string[] $items
738
     */
739 7
    private function generateJs(array $items): string
740
    {
741 7
        $lines = [];
742
743 7
        $js = [];
744 7
        foreach ($items as $item) {
745 7
            if ($item instanceof Script) {
746 4
                if ($js !== []) {
747 3
                    $lines[] = Html::script(implode("\n", $js))->render();
748 3
                    $js = [];
749
                }
750 4
                $lines[] = $item->render();
751
            } else {
752 6
                $js[] = $item;
753
            }
754
        }
755 7
        if ($js !== []) {
756 5
            $lines[] = Html::script(implode("\n", $js))->render();
757
        }
758
759 7
        return implode("\n", $lines);
760
    }
761
762
    /**
763
     * @param Script[]|string[] $items
764
     */
765 1
    private function generateJsWithoutTag(array $items): string
766
    {
767 1
        $js = [];
768 1
        foreach ($items as $item) {
769 1
            $js[] = $item instanceof Script ? $item->getContent() : $item;
770
        }
771 1
        return implode("\n", $js);
772
    }
773
}
774