Completed
Pull Request — master (#56)
by
unknown
02:01
created

WebView::head()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Yiisoft\View;
5
6
use BadFunctionCallException;
7
use Psr\Container\ContainerInterface;
8
use Psr\EventDispatcher\EventDispatcherInterface;
9
use Psr\Log\LoggerInterface;
10
use Yiisoft\Arrays\ArrayHelper;
11
use Yiisoft\Asset\AssetBundle;
12
use Yiisoft\Asset\AssetManager;
13
use Yiisoft\Di\Container;
14
use Yiisoft\Html\Html;
15
use Yiisoft\View\Event\BodyBegin;
16
use Yiisoft\View\Event\BodyEnd;
17
use Yiisoft\View\Event\PageEnd;
18
use Yiisoft\Widget\Widget;
19
20
/**
21
 * View represents a view object in the MVC pattern.
22
 *
23
 * View provides a set of methods (e.g. {@see render()} for rendering purpose.
24
 *
25
 * You can modify its configuration by adding an array to your application config under `components` as it is shown in
26
 * the following example:
27
 *
28
 * ```php
29
 * 'view' => [
30
 *     'theme' => 'app\themes\MyTheme',
31
 *     'renderers' => [
32
 *         // you may add Smarty or Twig renderer here
33
 *     ]
34
 *     // ...
35
 * ]
36
 * ```
37
 *
38
 * For more details and usage information on View, see the [guide article on views](guide:structure-views).
39
 */
40
class WebView extends View
41
{
42
    /**
43
     * The location of registered JavaScript code block or files.
44
     * This means the location is in the head section.
45
     */
46
    public const POS_HEAD = 1;
47
48
    /**
49
     * The location of registered JavaScript code block or files.
50
     * This means the location is at the beginning of the body section.
51
     */
52
    public const POS_BEGIN = 2;
53
54
    /**
55
     * The location of registered JavaScript code block or files.
56
     * This means the location is at the end of the body section.
57
     */
58
    public const POS_END = 3;
59
60
    /**
61
     * The location of registered JavaScript code block.
62
     * This means the JavaScript code block will be executed when HTML document composition is ready.
63
     */
64
    public const POS_READY = 4;
65
66
    /**
67
     * The location of registered JavaScript code block.
68
     * This means the JavaScript code block will be executed when HTML page is completely loaded.
69
     */
70
    public const POS_LOAD = 5;
71
72
    /**
73
     * This is internally used as the placeholder for receiving the content registered for the head section.
74
     */
75
    private const PH_HEAD = '<![CDATA[YII-BLOCK-HEAD]]>';
76
77
    /**
78
     * This is internally used as the placeholder for receiving the content registered for the beginning of the body
79
     * section.
80
     */
81
    private const PH_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN]]>';
82
83
    /**
84
     * This is internally used as the placeholder for receiving the content registered for the end of the body section.
85
     */
86
    private const PH_BODY_END = '<![CDATA[YII-BLOCK-BODY-END]]>';
87
88
    /**
89
     * The widgets that are currently being rendered (not ended). This property is maintained by {@see beginWidget()}
90
     * and {@see endWidget()} methods.
91
     *
92
     * @var Widget[] $stack
93
     */
94
    protected $stack;
95
96
    /**
97
     * @var AssetBundle[] list of the registered asset bundles. The keys are the bundle names, and the values
98
     * are the registered {@see AssetBundle} objects.
99
     *
100
     * {@see registerAssetBundle()}
101
     */
102
    private $assetBundles = [];
103
104
    /**
105
     * @var AssetManager $assetManager
106
     */
107
    private $assetManager;
108
109
    /**
110
     * @var string the page title
111
     */
112
    private $title;
0 ignored issues
show
introduced by
The private property $title is not used, and could be removed.
Loading history...
113
114
    /**
115
     * @var array the registered meta tags.
116
     *
117
     * {@see registerMetaTag()}
118
     */
119
    private $metaTags = [];
120
121
    /**
122
     * @var array the registered link tags.
123
     *
124
     * {@see registerLinkTag()}
125
     */
126
    private $linkTags = [];
127
128
    /**
129
     * @var array the registered CSS code blocks.
130
     *
131
     * {@see registerCss()}
132
     */
133
    private $css = [];
134
135
    /**
136
     * @var array the registered CSS files.
137
     *
138
     * {@see registerCssFile()}
139
     */
140
    private $cssFiles = [];
141
142
    /**
143
     * @var array the registered JS code blocks
144
     *
145
     * {@see registerJs()}
146
     */
147
    private $js = [];
148
149
    /**
150
     * @var array the registered JS files.
151
     *
152
     * {@see registerJsFile()}
153
     */
154
    private $jsFiles = [];
155
156
    /**
157
     * @var ContainerInterface|Container
158
     */
159
    private $container;
160
161 80
    public function __construct(
162
        string $basePath,
163
        Theme $theme,
164
        EventDispatcherInterface $eventDispatcher,
165
        LoggerInterface $logger,
166
        ContainerInterface $container
167
    ) {
168 80
        parent::__construct($basePath, $theme, $eventDispatcher, $logger);
169 80
        $this->container = $container;
170
    }
171
172
    /**
173
     * @param string $class
174
     * @return Widget
175
     */
176 25
    public function widget(string $class): Widget
177
    {
178 25
        if ($this->container->has($class)) {
179 2
            return $this->container->get($class);
180
        }
181
182 25
        $this->container->set($class, ['__class' => $class]);
0 ignored issues
show
Bug introduced by
The method set() does not exist on Psr\Container\ContainerInterface. It seems like you code against a sub-type of Psr\Container\ContainerInterface such as Yiisoft\Di\Container or Yiisoft\Factory\Factory. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

182
        $this->container->/** @scrutinizer ignore-call */ 
183
                          set($class, ['__class' => $class]);
Loading history...
183
184 25
        return $this->container->get($class);
185
    }
186
187
    /**
188
     * Begins a widget.
189
     *
190
     * This method creates an instance of the $class widget. It will apply the configuration to the created instance.
191
     * A matching {@see endWidget()} call should be called later. As some widgets may use output buffering, the {@see endWidget()}
192
     * call should be made in the same view to avoid breaking the nesting of output buffers.
193
     *
194
     * @param string $class
195
     * @return Widget the newly created widget instance.
196
     *
197
     * {@see endWidget()}
198
     */
199 8
    public function beginWidget(string $class): Widget
200
    {
201 8
        $widget = $this->widget($class);
202 8
        $this->stack[] = $widget;
203 8
        return $widget;
204
    }
205
206
    /**
207
     * Ends a widget
208
     *
209
     * Note that the rendering result of the widget is directly echoed out
210
     *
211
     * @param string $class
212
     * @return Widget the widget instance that is ended
213
     *
214
     * {@see beginWidget()}
215
     */
216 9
    public function endWidget(string $class): Widget
217
    {
218 9
        if (!empty($this->stack)) {
219 8
            $widget = array_pop($this->stack);
220
221 8
            if (get_class($widget) === $class) {
222
                /* @var $widget Widget */
223 7
                if ($widget->beforeRun()) {
224 7
                    $result = $widget->run();
225 7
                    $result = $widget->afterRun($result);
226 7
                    echo $result;
227
                }
228
229 7
                return $widget;
230
            }
231
232 1
            throw new BadFunctionCallException('Expecting end() of ' . get_class($widget) . ', found ' . $class);
233
        }
234
235 1
        throw new BadFunctionCallException('Unexpected ' . $class . '::end() call. A matching begin() is not found.');
236
    }
237
238
    /**
239
     * Marks the position of an HTML head section.
240
     */
241 32
    public function head(): void
242
    {
243 32
        echo self::PH_HEAD;
244
    }
245
246
    /**
247
     * Marks the beginning of an HTML body section.
248
     */
249 32
    public function beginBody(): void
250
    {
251 32
        echo self::PH_BODY_BEGIN;
252 32
        $this->eventDispatcher->dispatch(new BodyBegin($this->getViewFile()));
0 ignored issues
show
Bug introduced by
It seems like $this->getViewFile() can also be of type boolean; however, parameter $file of Yiisoft\View\Event\BodyBegin::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

252
        $this->eventDispatcher->dispatch(new BodyBegin(/** @scrutinizer ignore-type */ $this->getViewFile()));
Loading history...
253
    }
254
255
    /**
256
     * Marks the ending of an HTML body section.
257
     */
258 32
    public function endBody(): void
259
    {
260 32
        $this->eventDispatcher->dispatch(new BodyEnd($this->getViewFile()));
0 ignored issues
show
Bug introduced by
It seems like $this->getViewFile() can also be of type boolean; however, parameter $file of Yiisoft\View\Event\BodyEnd::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

260
        $this->eventDispatcher->dispatch(new BodyEnd(/** @scrutinizer ignore-type */ $this->getViewFile()));
Loading history...
261 32
        echo self::PH_BODY_END;
262
263 32
        foreach (array_keys($this->assetBundles) as $bundle) {
264 8
            $this->registerAssetFiles($bundle);
265
        }
266
    }
267
268
    /**
269
     * Marks the ending of an HTML page.
270
     *
271
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
272
     * {@see POS_READY} and {@see POS_LOAD} positions will be rendered at the end of the view like
273
     * normal scripts.
274
     */
275 32
    public function endPage($ajaxMode = false): void
276
    {
277 32
        $this->eventDispatcher->dispatch(new PageEnd($this->getViewFile()));
0 ignored issues
show
Bug introduced by
It seems like $this->getViewFile() can also be of type boolean; however, parameter $file of Yiisoft\View\Event\PageEnd::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

277
        $this->eventDispatcher->dispatch(new PageEnd(/** @scrutinizer ignore-type */ $this->getViewFile()));
Loading history...
278
279 32
        $content = ob_get_clean();
280
281 32
        echo strtr($content, [
282 32
            self::PH_HEAD => $this->renderHeadHtml(),
283 32
            self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(),
284 32
            self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode),
285
        ]);
286
287 32
        $this->clear();
288
    }
289
290
    /**
291
     * Renders a view in response to an AJAX request.
292
     *
293
     * This method is similar to {@see render()} except that it will surround the view being rendered with the calls of
294
     * {@see beginPage()}, {@see head()}, {@see beginBody()}, {@see endBody()} and {@see endPage()}. By doing so, the
295
     * method is able to inject into the rendering result with JS/CSS scripts and files that are registered with the
296
     * view.
297
     *
298
     * @param string $view the view name. Please refer to [[render()]] on how to specify this parameter.
299
     * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view
300
     * file.
301
     * @param object $context the context that the view should use for rendering the view. If null, existing [[context]]
302
     * will be used.
303
     *
304
     * @return string the rendering result
305
     *
306
     * {@see render()}
307
     */
308
    public function renderAjax(string $view, array $params = [], $context = null): string
309
    {
310
        $viewFile = $this->findViewFile($view, $context);
0 ignored issues
show
Bug introduced by
The method findViewFile() does not exist on Yiisoft\View\WebView. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

310
        /** @scrutinizer ignore-call */ 
311
        $viewFile = $this->findViewFile($view, $context);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
311
312
        ob_start();
313
        ob_implicit_flush(0);
314
315
        $this->beginPage();
316
        $this->head();
317
        $this->beginBody();
318
        echo $this->renderFile($viewFile, $params, $context);
319
        $this->endBody();
320
        $this->endPage(true);
321
322
        return ob_get_clean();
323
    }
324
325
    /**
326
     * Registers the asset manager being used by this view object.
327
     *
328
     * @return array the asset manager. Defaults to the "assetManager" application component.
329
     */
330 10
    public function getAssetBundles(): array
331
    {
332 10
        return $this->assetBundles;
333
    }
334
335
    /**
336
     * Registers the asset manager being used by this view object.
337
     *
338
     * @return AssetManager the asset manager. Defaults to the "assetManager" application component.
339
     */
340 33
    public function getAssetManager(): AssetManager
341
    {
342 33
        return $this->assetManager;
343
    }
344
345
    /**
346
     * Sets the asset manager.
347
     *
348
     * @param AssetManager $value the asset manager
349
     *
350
     * @return void
351
     */
352 80
    public function setAssetManager(AssetManager $value): void
353
    {
354 80
        $this->assetManager = $value;
355
    }
356
357
    /**
358
     * Clears up the registered meta tags, link tags, css/js scripts and files.
359
     *
360
     * @return void
361
     */
362 32
    public function clear(): void
363
    {
364 32
        $this->metaTags = [];
365 32
        $this->linkTags = [];
366 32
        $this->css = [];
367 32
        $this->cssFiles = [];
368 32
        $this->js = [];
369 32
        $this->jsFiles = [];
370 32
        $this->assetBundles = [];
371
    }
372
373
    /**
374
     * Registers all files provided by an asset bundle including depending bundles files.
375
     *
376
     * Removes a bundle from {@see assetBundles} once files are registered.
377
     *
378
     * @param string $name name of the bundle to register
379
     *
380
     * @return void
381
     */
382 8
    protected function registerAssetFiles(string $name): void
383
    {
384 8
        if (!isset($this->assetBundles[$name])) {
385 7
            return;
386
        }
387
388 8
        $bundle = $this->assetBundles[$name];
389
390 8
        if ($bundle) {
0 ignored issues
show
introduced by
$bundle is of type Yiisoft\Asset\AssetBundle, thus it always evaluated to true.
Loading history...
391 8
            foreach ($bundle->depends as $dep) {
392 7
                $this->registerAssetFiles($dep);
393
            }
394 8
            $bundle->registerAssetFiles($this);
395
        }
396
397 8
        unset($this->assetBundles[$name]);
398
    }
399
400
    /**
401
     * Registers the named asset bundle.
402
     *
403
     * All dependent asset bundles will be registered.
404
     *
405
     * @param string $name the class name of the asset bundle (without the leading backslash)
406
     * @param int|null $position if set, this forces a minimum position for javascript files. This will adjust depending
407
     * assets javascript file position or fail if requirement can not be met. If this is null, asset
408
     * bundles position settings will not be changed.
409
     *
410
     * {@see registerJsFile()} for more details on javascript position.
411
     *
412
     * @throws \RuntimeException if the asset bundle does not exist or a circular dependency is detected
413
     *
414
     * @return AssetBundle the registered asset bundle instance
415
     */
416 12
    public function registerAssetBundle(string $name, ?int $position = null): AssetBundle
417
    {
418 12
        if (!isset($this->assetBundles[$name])) {
419 12
            $bundle = $this->getAssetManager()->getBundle($name);
420
421 12
            $this->assetBundles[$name] = false;
422
423
            // register dependencies
424
425 12
            $pos = $bundle->jsOptions['position'] ?? null;
426
427 12
            foreach ($bundle->depends as $dep) {
428 10
                $this->registerAssetBundle($dep, $pos);
429
            }
430
431 11
            $this->assetBundles[$name] = $bundle;
432 8
        } elseif ($this->assetBundles[$name] === false) {
0 ignored issues
show
introduced by
The condition $this->assetBundles[$name] === false is always false.
Loading history...
433 1
            throw new \RuntimeException("A circular dependency is detected for bundle '$name'.");
434
        } else {
435 7
            $bundle = $this->assetBundles[$name];
436
        }
437
438 11
        if ($position !== null) {
439 8
            $pos = $bundle->jsOptions['position'] ?? null;
440
441 8
            if ($pos === null) {
442 8
                $bundle->jsOptions['position'] = $pos = $position;
443 2
            } elseif ($pos > $position) {
444 2
                throw new \RuntimeException("An asset bundle that depends on '$name' has a higher javascript file position configured than '$name'.");
445
            }
446
447
            // update position for all dependencies
448 8
            foreach ($bundle->depends as $dep) {
449 6
                $this->registerAssetBundle($dep, $pos);
450
            }
451
        }
452
453 11
        return $bundle;
454
    }
455
456
    /**
457
     * Registers a meta tag.
458
     *
459
     * For example, a description meta tag can be added like the following:
460
     *
461
     * ```php
462
     * $view->registerMetaTag([
463
     *     'name' => 'description',
464
     *     'content' => 'This website is about funny raccoons.'
465
     * ]);
466
     * ```
467
     *
468
     * will result in the meta tag `<meta name="description" content="This website is about funny raccoons.">`.
469
     *
470
     * @param array $options the HTML attributes for the meta tag.
471
     * @param string $key the key that identifies the meta tag. If two meta tags are registered with the same key, the
472
     * latter will overwrite the former. If this is null, the new meta tag will be appended to the
473
     * existing ones.
474
     *
475
     * @return void
476
     */
477
    public function registerMetaTag(array $options, string $key = null): void
478
    {
479
        if ($key === null) {
480
            $this->metaTags[] = Html::tag('meta', '', $options);
481
        } else {
482
            $this->metaTags[$key] = Html::tag('meta', '', $options);
483
        }
484
    }
485
486
    /**
487
     * Registers a link tag.
488
     *
489
     * For example, a link tag for a custom [favicon](http://www.w3.org/2005/10/howto-favicon) can be added like the
490
     * following:
491
     *
492
     * ```php
493
     * $view->registerLinkTag(['rel' => 'icon', 'type' => 'image/png', 'href' => '/myicon.png']);
494
     * ```
495
     *
496
     * which will result in the following HTML: `<link rel="icon" type="image/png" href="/myicon.png">`.
497
     *
498
     * **Note:** To register link tags for CSS stylesheets, use [[registerCssFile()]] instead, which has more options
499
     * for this kind of link tag.
500
     *
501
     * @param array $options the HTML attributes for the link tag.
502
     * @param string|null $key the key that identifies the link tag. If two link tags are registered with the same
503
     * key, the latter will overwrite the former. If this is null, the new link tag will be appended
504
     * to the existing ones.
505
     */
506
    public function registerLinkTag(array $options, ?string $key = null): void
507
    {
508
        if ($key === null) {
509
            $this->linkTags[] = Html::tag('link', '', $options);
510
        } else {
511
            $this->linkTags[$key] = Html::tag('link', '', $options);
512
        }
513
    }
514
515
    /**
516
     * Registers CSRF meta tags.
517
     *
518
     * They are rendered dynamically to retrieve a new CSRF token for each request.
519
     *
520
     * ```php
521
     * $view->registerCsrfMetaTags();
522
     * ```
523
     *
524
     * The above code will result in `<meta name="csrf-param" content="[Yiisoft\Web\Request::$csrfParam]">` and
525
     * `<meta name="csrf-token" content="tTNpWKpdy-bx8ZmIq9R72...K1y8IP3XGkzZA==">` added to the page.
526
     *
527
     * Note: Hidden CSRF input of ActiveForm will be automatically refreshed by calling `window.yii.refreshCsrfToken()`
528
     * from `yii.js`.
529
     */
530
    public function registerCsrfMetaTags(): void
531
    {
532
        $this->metaTags['csrf_meta_tags'] = $this->renderDynamic('return Yiisoft\Html\Html::csrfMetaTags();');
533
    }
534
535
    /**
536
     * Registers a CSS code block.
537
     *
538
     * @param string $css the content of the CSS code block to be registered
539
     * @param array $options the HTML attributes for the `<style>`-tag.
540
     * @param string $key the key that identifies the CSS code block. If null, it will use $css as the key. If two CSS
541
     * code blocks are registered with the same key, the latter will overwrite the former.
542
     */
543
    public function registerCss(string $css, array $options = [], string $key = null): void
544
    {
545
        $key = $key ?: md5($css);
546
        $this->css[$key] = Html::style($css, $options);
547
    }
548
549
    /**
550
     * Registers a CSS file.
551
     *
552
     * This method should be used for simple registration of CSS files. If you want to use features of
553
     * {@see AssetManager} like appending timestamps to the URL and file publishing options, use {@see AssetBundle}
554
     * and {@see registerAssetBundle()} instead.
555
     *
556
     * @param string $url the CSS file to be registered.
557
     * @param array $options the HTML attributes for the link tag. Please refer to {@see \Yiisoft\Html\Html::cssFile()}
558
     * for the supported options. The following options are specially handled and are not treated as HTML
559
     * attributes:
560
     *
561
     *   - `depends`: array, specifies the names of the asset bundles that this CSS file depends on.
562
     *
563
     * @param string $key the key that identifies the CSS script file. If null, it will use $url as the key. If two CSS
564
     * files are registered with the same key, the latter will overwrite the former.
565
     *
566
     * @return void
567
     */
568 18
    public function registerCssFile(string $url, array $options = [], string $key = null): void
569
    {
570 18
        $key = $key ?: $url;
571
572 18
        $depends = ArrayHelper::remove($options, 'depends', []);
573
574 18
        if (empty($depends)) {
575 18
            $this->cssFiles[$key] = Html::cssFile($url, $options);
576
        } else {
577
            $bundle = $this->createBundle([
578
                'baseUrl' => '',
579
                'css' => [strncmp($url, '//', 2) === 0 ? $url : ltrim($url, '/')],
580
                'cssOptions' => $options,
581
                'depends' => (array) $depends,
582
            ]);
583
            $bundles = [$key => $bundle];
0 ignored issues
show
Unused Code introduced by
The assignment to $bundles is dead and can be removed.
Loading history...
584
585
            $this->registerAssetBundle($key);
586
        }
587
    }
588
589
    /**
590
     * Registers a JS code block.
591
     *
592
     * @param string $js the JS code block to be registered
593
     * @param int $position the position at which the JS script tag should be inserted in a page.
594
     *
595
     * The possible values are:
596
     *
597
     * - [[POS_HEAD]]: in the head section
598
     * - [[POS_BEGIN]]: at the beginning of the body section
599
     * - [[POS_END]]: at the end of the body section. This is the default value.
600
     * - [[POS_LOAD]]: executed when HTML page is completely loaded.
601
     * - [[POS_READY]]: executed when HTML document composition is ready.
602
     *
603
     * @param string $key the key that identifies the JS code block. If null, it will use $js as the key. If two JS code
604
     * blocks are registered with the same key, the latter will overwrite the former.
605
     *
606
     * @return void
607
     */
608 1
    public function registerJs(string $js, int $position = self::POS_END, string $key = null): void
609
    {
610 1
        $key = $key ?: md5($js);
611 1
        $this->js[$position][$key] = $js;
612
    }
613
614
    /**
615
     * Registers a JS file.
616
     *
617
     * This method should be used for simple registration of JS files. If you want to use features of
618
     * {@see AssetManager} like appending timestamps to the URL and file publishing options, use {@see AssetBundle}
619
     * and {@see registerAssetBundle()} instead.
620
     *
621
     * @param string $url the JS file to be registered.
622
     * @param array $options the HTML attributes for the script tag. The following options are specially handled and
623
     * are not treated as HTML attributes:
624
     *
625
     * - `depends`: array, specifies the names of the asset bundles that this JS file depends on.
626
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
627
     *     * [[POS_HEAD]]: in the head section
628
     *     * [[POS_BEGIN]]: at the beginning of the body section
629
     *     * [[POS_END]]: at the end of the body section. This is the default value.
630
     *
631
     * Please refer to {@see \Yiisoft\Html\Html::jsFile()} for other supported options.
632
     *
633
     * @param string $key the key that identifies the JS script file. If null, it will use $url as the key. If two JS
634
     * files are registered with the same key at the same position, the latter will overwrite the former.
635
     * Note that position option takes precedence, thus files registered with the same key, but different
636
     * position option will not override each other.
637
     *
638
     * @return void
639
     */
640 19
    public function registerJsFile(string $url, array $options = [], string $key = null): void
641
    {
642 19
        $key = $key ?: $url;
643
644 19
        $depends = ArrayHelper::remove($options, 'depends', []);
645
646 19
        if (empty($depends)) {
647 19
            $position = ArrayHelper::remove($options, 'position', self::POS_END);
648 19
            $this->jsFiles[$position][$key] = Html::jsFile($url, $options);
649
        } else {
650
            $bundle = $this->createBundle([
651
                'baseUrl' => '',
652
                'js' => [strncmp($url, '//', 2) === 0 ? $url : ltrim($url, '/')],
653
                'jsOptions' => $options,
654
                'depends' => (array) $depends,
655
            ]);
656
            $bundles = [$key => $bundle];
0 ignored issues
show
Unused Code introduced by
The assignment to $bundles is dead and can be removed.
Loading history...
657
            $this->registerAssetBundle($key);
658
        }
659
    }
660
661
    /**
662
     * Registers a JS code block defining a variable. The name of variable will be used as key, preventing duplicated
663
     * variable names.
664
     *
665
     * @param string $name Name of the variable
666
     * @param array|string $value Value of the variable
667
     * @param int $position the position in a page at which the JavaScript variable should be inserted.
668
     *
669
     * The possible values are:
670
     *
671
     * - [[POS_HEAD]]: in the head section. This is the default value.
672
     * - [[POS_BEGIN]]: at the beginning of the body section.
673
     * - [[POS_END]]: at the end of the body section.
674
     * - [[POS_LOAD]]: enclosed within jQuery(window).load().
675
     *   Note that by using this position, the method will automatically register the jQuery js file.
676
     * - [[POS_READY]]: enclosed within jQuery(document).ready().
677
     *   Note that by using this position, the method will automatically register the jQuery js file.
678
     */
679 1
    public function registerJsVar(string $name, $value, int $position = self::POS_HEAD): void
680
    {
681 1
        $js = sprintf('var %s = %s;', $name, \Yiisoft\Json\Json::htmlEncode($value));
682 1
        $this->registerJs($js, $position, $name);
683
    }
684
685
    /**
686
     * Renders the content to be inserted in the head section.
687
     *
688
     * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files.
689
     *
690
     * @return string the rendered content
691
     */
692 32
    protected function renderHeadHtml(): string
693
    {
694 32
        $lines = [];
695 32
        if (!empty($this->metaTags)) {
696
            $lines[] = implode("\n", $this->metaTags);
697
        }
698
699 32
        if (!empty($this->linkTags)) {
700
            $lines[] = implode("\n", $this->linkTags);
701
        }
702 32
        if (!empty($this->cssFiles)) {
703 18
            $lines[] = implode("\n", $this->cssFiles);
704
        }
705 32
        if (!empty($this->css)) {
706
            $lines[] = implode("\n", $this->css);
707
        }
708 32
        if (!empty($this->jsFiles[self::POS_HEAD])) {
709 3
            $lines[] = implode("\n", $this->jsFiles[self::POS_HEAD]);
710
        }
711 32
        if (!empty($this->js[self::POS_HEAD])) {
712 1
            $lines[] = Html::script(implode("\n", $this->js[self::POS_HEAD]));
713
        }
714
715 32
        return empty($lines) ? '' : implode("\n", $lines);
716
    }
717
718
    /**
719
     * Renders the content to be inserted at the beginning of the body section.
720
     *
721
     * The content is rendered using the registered JS code blocks and files.
722
     *
723
     * @return string the rendered content
724
     */
725 32
    protected function renderBodyBeginHtml(): string
726
    {
727 32
        $lines = [];
728 32
        if (!empty($this->jsFiles[self::POS_BEGIN])) {
729 3
            $lines[] = implode("\n", $this->jsFiles[self::POS_BEGIN]);
730
        }
731 32
        if (!empty($this->js[self::POS_BEGIN])) {
732
            $lines[] = Html::script(implode("\n", $this->js[self::POS_BEGIN]));
733
        }
734
735 32
        return empty($lines) ? '' : implode("\n", $lines);
736
    }
737
738
    /**
739
     * Renders the content to be inserted at the end of the body section.
740
     *
741
     * The content is rendered using the registered JS code blocks and files.
742
     *
743
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
744
     * [[POS_READY]] and [[POS_LOAD]] positions will be rendered at the end of the view like normal scripts.
745
     *
746
     * @return string the rendered content
747
     */
748 32
    protected function renderBodyEndHtml(bool $ajaxMode): string
749
    {
750 32
        $lines = [];
751
752 32
        if (!empty($this->jsFiles[self::POS_END])) {
753 15
            $lines[] = implode("\n", $this->jsFiles[self::POS_END]);
754
        }
755
756 32
        if ($ajaxMode) {
757
            $scripts = [];
758
            if (!empty($this->js[self::POS_END])) {
759
                $scripts[] = implode("\n", $this->js[self::POS_END]);
760
            }
761
            if (!empty($this->js[self::POS_READY])) {
762
                $scripts[] = implode("\n", $this->js[self::POS_READY]);
763
            }
764
            if (!empty($this->js[self::POS_LOAD])) {
765
                $scripts[] = implode("\n", $this->js[self::POS_LOAD]);
766
            }
767
            if (!empty($scripts)) {
768
                $lines[] = Html::script(implode("\n", $scripts));
769
            }
770
        } else {
771 32
            if (!empty($this->js[self::POS_END])) {
772
                $lines[] = Html::script(implode("\n", $this->js[self::POS_END]));
773
            }
774 32
            if (!empty($this->js[self::POS_READY])) {
775
                $js = "document.addEventListener('DOMContentLoaded', function(event) {\n" . implode("\n", $this->js[self::POS_READY]) . "\n});";
776
                $lines[] = Html::script($js, ['type' => 'text/javascript']);
777
            }
778 32
            if (!empty($this->js[self::POS_LOAD])) {
779
                $js = "window.addEventListener('load', function (event) {\n" . implode("\n", $this->js[self::POS_LOAD]) . "\n});";
780
                $lines[] = Html::script($js, ['type' => 'text/javascript']);
781
            }
782
        }
783
784 32
        return empty($lines) ? '' : implode("\n", $lines);
785
    }
786
787
    /**
788
     * @param array $options
789
     *
790
     * @return AssetBundle
791
     */
792
    private function createBundle(array $options): AssetBundle
793
    {
794
        $bundle = new AssetBundle();
795
        $bundle->baseUrl = $options['baseUrl'];
796
        $bundle->js = $options['js'];
797
        $bundle->jsOptions = $options['jsOptions'];
798
        $bundle->depends = $options['depends'];
799
800
        return $bundle;
801
    }
802
}
803