Passed
Pull Request — master (#195)
by Sergei
08:08 queued 05:57
created

WebView::createAfterRenderEvent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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