Passed
Pull Request — master (#138)
by Alexander
04:44 queued 02:08
created

WebView::registerJsVar()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\View;
6
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Html\Html;
9
use Yiisoft\Html\Tag\Script;
10
use Yiisoft\View\Event\BodyBegin;
11
use Yiisoft\View\Event\BodyEnd;
12
use Yiisoft\View\Event\PageEnd;
13
14
/**
15
 * View represents a view object in the MVC pattern.
16
 *
17
 * View provides a set of methods (e.g. {@see render()} for rendering purpose.
18
 *
19
 * You can modify its configuration by adding an array to your application config under `components` as it is shown in
20
 * the following example:
21
 *
22
 * ```php
23
 * 'view' => [
24
 *     'theme' => 'app\themes\MyTheme',
25
 *     'renderers' => [
26
 *         // you may add Smarty or Twig renderer here
27
 *     ]
28
 *     // ...
29
 * ]
30
 * ```
31
 *
32
 * For more details and usage information on View, see the [guide article on views](guide:structure-views).
33
 */
34
class WebView extends View
35
{
36
    /**
37
     * The location of registered JavaScript code block or files.
38
     * This means the location is in the head section.
39
     */
40
    public const POSITION_HEAD = 1;
41
42
    /**
43
     * The location of registered JavaScript code block or files.
44
     * This means the location is at the beginning of the body section.
45
     */
46
    public const POSITION_BEGIN = 2;
47
48
    /**
49
     * The location of registered JavaScript code block or files.
50
     * This means the location is at the end of the body section.
51
     */
52
    public const POSITION_END = 3;
53
54
    /**
55
     * The location of registered JavaScript code block.
56
     * This means the JavaScript code block will be executed when HTML document composition is ready.
57
     */
58
    public const POSITION_READY = 4;
59
60
    /**
61
     * The location of registered JavaScript code block.
62
     * This means the JavaScript code block will be executed when HTML page is completely loaded.
63
     */
64
    public const POSITION_LOAD = 5;
65
66
    /**
67
     * This is internally used as the placeholder for receiving the content registered for the head section.
68
     */
69
    private const PLACEHOLDER_HEAD = '<![CDATA[YII-BLOCK-HEAD-%s]]>';
70
71
    /**
72
     * This is internally used as the placeholder for receiving the content registered for the beginning of the body
73
     * section.
74
     */
75
    private const PLACEHOLDER_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN-%s]]>';
76
77
    /**
78
     * This is internally used as the placeholder for receiving the content registered for the end of the body section.
79
     */
80
    private const PLACEHOLDER_BODY_END = '<![CDATA[YII-BLOCK-BODY-END-%s]]>';
81
82
    /**
83
     * @var string the page title
84
     */
85
    private string $title = '';
86
87
    /**
88
     * @var array the registered meta tags.
89
     *
90
     * {@see registerMetaTag()}
91
     */
92
    private array $metaTags = [];
93
94
    /**
95
     * @var array the registered link tags.
96
     *
97
     * {@see registerLinkTag()}
98
     */
99
    private array $linkTags = [];
100
101
    /**
102
     * @var array the registered CSS code blocks.
103
     *
104
     * {@see registerCss()}
105
     */
106
    private array $css = [];
107
108
    /**
109
     * @var array the registered CSS files.
110
     *
111
     * {@see registerCssFile()}
112
     */
113
    private array $cssFiles = [];
114
115
    /**
116
     * @var array the registered JS code blocks
117
     * @psalm-var array<int, string[]|Script[]>
118
     *
119
     * {@see registerJs()}
120
     */
121
    private array $js = [];
122
123
    /**
124
     * @var array the registered JS files.
125
     *
126
     * {@see registerJsFile()}
127
     */
128
    private array $jsFiles = [];
129
130
    /**
131
     * Marks the position of an HTML head section.
132
     */
133 11
    public function head(): void
134
    {
135 11
        echo sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature());
136 11
    }
137
138
    /**
139
     * Marks the beginning of an HTML body section.
140
     */
141 11
    public function beginBody(): void
142
    {
143 11
        echo sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature());
144 11
        $this->eventDispatcher->dispatch(new BodyBegin($this->getViewFile()));
145 11
    }
146
147
    /**
148
     * Marks the ending of an HTML body section.
149
     */
150 11
    public function endBody(): void
151
    {
152 11
        $this->eventDispatcher->dispatch(new BodyEnd($this->getViewFile()));
153 11
        echo sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature());
154 11
    }
155
156
    /**
157
     * Marks the ending of an HTML page.
158
     *
159
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
160
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like
161
     * normal scripts.
162
     */
163 11
    public function endPage($ajaxMode = false): void
164
    {
165 11
        $this->eventDispatcher->dispatch(new PageEnd($this->getViewFile()));
166
167 11
        $content = ob_get_clean();
168
169 11
        echo strtr($content, [
170 11
            sprintf(self::PLACEHOLDER_HEAD, $this->getPlaceholderSignature()) => $this->renderHeadHtml(),
171 11
            sprintf(self::PLACEHOLDER_BODY_BEGIN, $this->getPlaceholderSignature()) => $this->renderBodyBeginHtml(),
172 11
            sprintf(self::PLACEHOLDER_BODY_END, $this->getPlaceholderSignature()) => $this->renderBodyEndHtml($ajaxMode),
173
        ]);
174
175 11
        $this->clear();
176 11
    }
177
178
    /**
179
     * Renders a view in response to an AJAX request.
180
     *
181
     * This method is similar to {@see render()} except that it will surround the view being rendered with the calls of
182
     * {@see beginPage()}, {@see head()}, {@see beginBody()}, {@see endBody()} and {@see endPage()}. By doing so, the
183
     * method is able to inject into the rendering result with JS/CSS scripts and files that are registered with the
184
     * view.
185
     *
186
     * @param string $view the view name. Please refer to {@see render()} on how to specify this parameter.
187
     * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view
188
     * file.
189
     *
190
     * @return string the rendering result
191
     *
192
     * {@see render()}
193
     */
194 2
    public function renderAjax(string $view, array $params = []): string
195
    {
196 2
        $viewFile = $this->findTemplateFile($view);
197
198 2
        ob_start();
199 2
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
200
201 2
        $this->beginPage();
202 2
        $this->head();
203 2
        $this->beginBody();
204 2
        echo $this->renderFile($viewFile, $params);
205 2
        $this->endBody();
206 2
        $this->endPage(true);
207
208 2
        return ob_get_clean();
209
    }
210
211
    /**
212
     * Clears up the registered meta tags, link tags, css/js scripts and files.
213
     */
214 11
    public function clear(): void
215
    {
216 11
        $this->metaTags = [];
217 11
        $this->linkTags = [];
218 11
        $this->css = [];
219 11
        $this->cssFiles = [];
220 11
        $this->js = [];
221 11
        $this->jsFiles = [];
222 11
    }
223
224
    /**
225
     * Registers a meta tag.
226
     *
227
     * For example, a description meta tag can be added like the following:
228
     *
229
     * ```php
230
     * $view->registerMetaTag([
231
     *     'name' => 'description',
232
     *     'content' => 'This website is about funny raccoons.'
233
     * ]);
234
     * ```
235
     *
236
     * will result in the meta tag `<meta name="description" content="This website is about funny raccoons.">`.
237
     *
238
     * @param array $options the HTML attributes for the meta tag.
239
     * @param string $key the key that identifies the meta tag. If two meta tags are registered with the same key, the
240
     * latter will overwrite the former. If this is null, the new meta tag will be appended to the
241
     * existing ones.
242
     */
243 1
    public function registerMetaTag(array $options, string $key = null): void
244
    {
245 1
        if ($key === null) {
246 1
            $this->metaTags[] = Html::meta()->attributes($options)->render();
247
        } else {
248
            $this->metaTags[$key] = Html::meta()->attributes($options)->render();
249
        }
250 1
    }
251
252
    /**
253
     * Registers a link tag.
254
     *
255
     * For example, a link tag for a custom [favicon](http://www.w3.org/2005/10/howto-favicon) can be added like the
256
     * following:
257
     *
258
     * ```php
259
     * $view->registerLinkTag(['rel' => 'icon', 'type' => 'image/png', 'href' => '/myicon.png']);
260
     * ```
261
     *
262
     * which will result in the following HTML: `<link rel="icon" type="image/png" href="/myicon.png">`.
263
     *
264
     * **Note:** To register link tags for CSS stylesheets, use {@see registerCssFile()]} instead, which has more
265
     * options for this kind of link tag.
266
     *
267
     * @param array $options the HTML attributes for the link tag.
268
     * @param string|null $key the key that identifies the link tag. If two link tags are registered with the same
269
     * key, the latter will overwrite the former. If this is null, the new link tag will be appended
270
     * to the existing ones.
271
     */
272 1
    public function registerLinkTag(array $options, ?string $key = null): void
273
    {
274 1
        if ($key === null) {
275 1
            $this->linkTags[] = Html::link()->attributes($options)->render();
276
        } else {
277
            $this->linkTags[$key] = Html::link()->attributes($options)->render();
278
        }
279 1
    }
280
281
    /**
282
     * Registers a CSS code block.
283
     *
284
     * @param string $css the content of the CSS code block to be registered
285
     * @param array $options the HTML attributes for the `<style>`-tag.
286
     * @param string $key the key that identifies the CSS code block. If null, it will use $css as the key. If two CSS
287
     * code blocks are registered with the same key, the latter will overwrite the former.
288
     */
289 1
    public function registerCss(string $css, array $options = [], string $key = null): void
290
    {
291 1
        $key = $key ?: md5($css);
292 1
        $this->css[$key] = Html::style($css, $options)->render();
293 1
    }
294
295
    /**
296
     * Registers a CSS file.
297
     *
298
     * This method should be used for simple registration of CSS files. If you want to use features of
299
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
300
     * {@see \Yiisoft\Assets\AssetBundle}.
301
     *
302
     * @param string $url the CSS file to be registered.
303
     * @param array $options the HTML attributes for the link tag. Please refer to {@see \Yiisoft\Html\Html::cssFile()}
304
     * for the supported options.
305
     * @param string $key the key that identifies the CSS script file. If null, it will use $url as the key. If two CSS
306
     * files are registered with the same key, the latter will overwrite the former.
307
     */
308 1
    public function registerCssFile(string $url, array $options = [], string $key = null): void
309
    {
310 1
        $key = $key ?: $url;
311
312 1
        $this->cssFiles[$key] = Html::cssFile($url, $options)->render();
313 1
    }
314
315
    /**
316
     * Registers a JS code block.
317
     *
318
     * @param string $js the JS code block to be registered
319
     * @param int $position the position at which the JS script tag should be inserted in a page.
320
     *
321
     * The possible values are:
322
     *
323
     * - {@see POSITION_HEAD}: in the head section
324
     * - {@see POSITION_BEGIN}: at the beginning of the body section
325
     * - {@see POSITION_END}: at the end of the body section. This is the default value.
326
     * - {@see POSITION_LOAD}: executed when HTML page is completely loaded.
327
     * - {@see POSITION_READY}: executed when HTML document composition is ready.
328
     * @param string $key the key that identifies the JS code block. If null, it will use $js as the key. If two JS code
329
     * blocks are registered with the same key, the latter will overwrite the former.
330
     */
331 3
    public function registerJs(string $js, int $position = self::POSITION_END, string $key = null): void
332
    {
333 3
        $key = $key ?: md5($js);
334 3
        $this->js[$position][$key] = $js;
335 3
    }
336
337
    /**
338
     * Register a `script` tag
339
     *
340
     * @see registerJs()
341
     */
342 3
    public function registerScriptTag(Script $script, int $position = self::POSITION_END, string $key = null): void
343
    {
344 3
        $key = $key ?: md5($script->render());
345 3
        $this->js[$position][$key] = $script;
346 3
    }
347
348
    /**
349
     * Registers a JS file.
350
     *
351
     * This method should be used for simple registration of JS files. If you want to use features of
352
     * {@see \Yiisoft\Assets\AssetManager} like appending timestamps to the URL and file publishing options, use
353
     * {@see \Yiisoft\Assets\AssetBundle}.
354
     *
355
     * @param string $url the JS file to be registered.
356
     * @param array $options the HTML attributes for the script tag. The following options are specially handled and
357
     * are not treated as HTML attributes:
358
     *
359
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
360
     *     * {@see POSITION_HEAD}: in the head section
361
     *     * {@see POSITION_BEGIN}: at the beginning of the body section
362
     *     * {@see POSITION_END}: at the end of the body section. This is the default value.
363
     *
364
     * Please refer to {@see \Yiisoft\Html\Html::javaScriptFile()} for other supported options.
365
     * @param string $key the key that identifies the JS script file. If null, it will use $url as the key. If two JS
366
     * files are registered with the same key at the same position, the latter will overwrite the former.
367
     * Note that position option takes precedence, thus files registered with the same key, but different
368
     * position option will not override each other.
369
     */
370 1
    public function registerJsFile(string $url, array $options = [], string $key = null): void
371
    {
372 1
        $key = $key ?: $url;
373
374 1
        $position = ArrayHelper::remove($options, 'position', self::POSITION_END);
375 1
        $this->jsFiles[$position][$key] = Html::javaScriptFile($url, $options)->render();
376 1
    }
377
378
    /**
379
     * Registers a JS code block defining a variable. The name of variable will be used as key, preventing duplicated
380
     * variable names.
381
     *
382
     * @param string $name Name of the variable
383
     * @param array|string $value Value of the variable
384
     * @param int $position the position in a page at which the JavaScript variable should be inserted.
385
     *
386
     * The possible values are:
387
     *
388
     * - {@see POSITION_HEAD}: in the head section. This is the default value.
389
     * - {@see POSITION_BEGIN}: at the beginning of the body section.
390
     * - {@see POSITION_END}: at the end of the body section.
391
     * - {@see POSITION_LOAD}: enclosed within jQuery(window).load().
392
     *   Note that by using this position, the method will automatically register the jQuery js file.
393
     * - {@see POSITION_READY}: enclosed within jQuery(document).ready().
394
     *   Note that by using this position, the method will automatically register the jQuery js file.
395
     */
396 1
    public function registerJsVar(string $name, $value, int $position = self::POSITION_HEAD): void
397
    {
398 1
        $js = sprintf('var %s = %s;', $name, \Yiisoft\Json\Json::htmlEncode($value));
399 1
        $this->registerJs($js, $position, $name);
400 1
    }
401
402
    /**
403
     * Renders the content to be inserted in the head section.
404
     *
405
     * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files.
406
     *
407
     * @return string the rendered content
408
     */
409 11
    protected function renderHeadHtml(): string
410
    {
411 11
        $lines = [];
412 11
        if (!empty($this->metaTags)) {
413 1
            $lines[] = implode("\n", $this->metaTags);
414
        }
415
416 11
        if (!empty($this->linkTags)) {
417 1
            $lines[] = implode("\n", $this->linkTags);
418
        }
419 11
        if (!empty($this->cssFiles)) {
420 1
            $lines[] = implode("\n", $this->cssFiles);
421
        }
422 11
        if (!empty($this->css)) {
423 1
            $lines[] = implode("\n", $this->css);
424
        }
425 11
        if (!empty($this->jsFiles[self::POSITION_HEAD])) {
426 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_HEAD]);
427
        }
428 11
        if (!empty($this->js[self::POSITION_HEAD])) {
429 1
            $lines[] = $this->generateJs($this->js[self::POSITION_HEAD]);
430
        }
431
432 11
        return empty($lines) ? '' : implode("\n", $lines);
433
    }
434
435
    /**
436
     * Renders the content to be inserted at the beginning of the body section.
437
     *
438
     * The content is rendered using the registered JS code blocks and files.
439
     *
440
     * @return string the rendered content
441
     */
442 11
    protected function renderBodyBeginHtml(): string
443
    {
444 11
        $lines = [];
445 11
        if (!empty($this->jsFiles[self::POSITION_BEGIN])) {
446 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_BEGIN]);
447
        }
448 11
        if (!empty($this->js[self::POSITION_BEGIN])) {
449
            $lines[] = $this->generateJs($this->js[self::POSITION_BEGIN]);
450
        }
451
452 11
        return empty($lines) ? '' : implode("\n", $lines);
453
    }
454
455
    /**
456
     * Renders the content to be inserted at the end of the body section.
457
     *
458
     * The content is rendered using the registered JS code blocks and files.
459
     *
460
     * @param bool $ajaxMode whether the view is rendering in AJAX mode. If true, the JS scripts registered at
461
     * {@see POSITION_READY} and {@see POSITION_LOAD} positions will be rendered at the end of the view like normal
462
     * scripts.
463
     *
464
     * @return string the rendered content
465
     */
466 11
    protected function renderBodyEndHtml(bool $ajaxMode): string
467
    {
468 11
        $lines = [];
469
470 11
        if (!empty($this->jsFiles[self::POSITION_END])) {
471 1
            $lines[] = implode("\n", $this->jsFiles[self::POSITION_END]);
472
        }
473
474 11
        if ($ajaxMode) {
475 2
            $scripts = array_merge(
476 2
                $this->js[self::POSITION_END] ?? [],
477 2
                $this->js[self::POSITION_READY] ?? [],
478 2
                $this->js[self::POSITION_LOAD] ?? [],
479
            );
480 2
            if (!empty($scripts)) {
481 2
                $lines[] = $this->generateJs($scripts);
482
            }
483
        } else {
484 9
            if (!empty($this->js[self::POSITION_END])) {
485 2
                $lines[] = $this->generateJs($this->js[self::POSITION_END]);
486
            }
487 9
            if (!empty($this->js[self::POSITION_READY])) {
488
                $js = "document.addEventListener('DOMContentLoaded', function(event) {\n" .
489 1
                    $this->generateJsWithoutTag($this->js[self::POSITION_READY]) .
490 1
                    "\n});";
491 1
                $lines[] = Html::script($js)->render();
492
            }
493 9
            if (!empty($this->js[self::POSITION_LOAD])) {
494
                $js = "window.addEventListener('load', function (event) {\n" .
495
                    $this->generateJsWithoutTag($this->js[self::POSITION_LOAD]) .
496
                    "\n});";
497
                $lines[] = Html::script($js)->render();
498
            }
499
        }
500
501 11
        return empty($lines) ? '' : implode("\n", $lines);
502
    }
503
504
    /**
505
     * Get title in views.
506
     *
507
     * in Layout:
508
     *
509
     * ```php
510
     * <title><?= Html::encode($this->getTitle()) ?></title>
511
     * ```
512
     *
513
     * in Views:
514
     *
515
     * ```php
516
     * $this->setTitle('Web Application - Yii 3.0.');
517
     * ```
518
     *
519
     * @return string
520
     */
521
    public function getTitle(): string
522
    {
523
        return $this->title;
524
    }
525
526
    /**
527
     * It processes the CSS configuration generated by the asset manager and converts it into HTML code.
528
     *
529
     * @param array $cssFiles
530
     */
531
    public function setCssFiles(array $cssFiles): void
532
    {
533
        foreach ($cssFiles as $key => $value) {
534
            $this->registerCssFile(
535
                $cssFiles[$key]['url'],
536
                $cssFiles[$key]['attributes']
537
            );
538
        }
539
    }
540
541
    /**
542
     * It processes the JS configuration generated by the asset manager and converts it into HTML code.
543
     *
544
     * @param array $jsFiles
545
     */
546
    public function setJsFiles(array $jsFiles): void
547
    {
548
        foreach ($jsFiles as $key => $value) {
549
            $this->registerJsFile(
550
                $jsFiles[$key]['url'],
551
                $jsFiles[$key]['attributes']
552
            );
553
        }
554
    }
555
556
    /**
557
     * It processes the JS strings generated by the asset manager.
558
     *
559
     * @param array $jsStrings
560
     */
561
    public function setJsStrings(array $jsStrings): void
562
    {
563
        foreach ($jsStrings as $value) {
564
            $this->registerJs(
565
                $value['string'],
566
                $value['attributes']['position']
567
            );
568
        }
569
    }
570
571
    /**
572
     * It processes the JS variables generated by the asset manager and converts it into JS code.
573
     *
574
     * @param array $jsVar
575
     */
576
    public function setJsVar(array $jsVar): void
577
    {
578
        foreach ($jsVar as $key => $value) {
579
            $this->registerJsVar(
580
                (string)$key,
581
                $value['variables'],
582
                $value['attributes']['position']
583
            );
584
        }
585
    }
586
587
    /**
588
     * Set title in views.
589
     *
590
     * {@see getTitle()}
591
     *
592
     * @param string $value
593
     */
594
    public function setTitle(string $value): void
595
    {
596
        $this->title = $value;
597
    }
598
599
    /**
600
     * @param Script[]|string[] $items
601
     */
602 4
    private function generateJs(array $items): string
603
    {
604 4
        $lines = [];
605
606 4
        $js = [];
607 4
        foreach ($items as $item) {
608 4
            if ($item instanceof Script) {
609 3
                if ($js !== []) {
610 2
                    $lines[] = Html::script(implode("\n", $js))->render();
611 2
                    $js = [];
612
                }
613 3
                $lines[] = $item->render();
614
            } else {
615 3
                $js[] = $item;
616
            }
617
        }
618 4
        if ($js !== []) {
619 2
            $lines[] = Html::script(implode("\n", $js))->render();
620
        }
621
622 4
        return implode("\n", $lines);
623
    }
624
625
    /**
626
     * @param Script[]|string[] $items
627
     */
628 1
    private function generateJsWithoutTag(array $items): string
629
    {
630 1
        $js = [];
631 1
        foreach ($items as $item) {
632 1
            $js[] = $item instanceof Script ? $item->getContent() : $item;
633
        }
634 1
        return implode("\n", $js);
635
    }
636
}
637