Completed
Push — master ( ba39fb...f489a4 )
by Andreas
26:01
created

midcom_helper_head::add_jscript()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 3
dl 0
loc 7
ccs 4
cts 5
cp 0.8
crap 2.032
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package midcom.helper
4
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
6
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
7
 */
8
9
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
10
use Symfony\Component\HttpKernel\KernelEvents;
11
12
/**
13
 * Helper functions for managing HTML head
14
 *
15
 * @package midcom.helper
16
 */
17
class midcom_helper_head
18
{
19
    /**
20
     * Array with all JavaScript declarations for the page's head.
21
     *
22
     * @var array
23
     */
24
    private $_jshead = [];
25
26
    /**
27
     * Array with all JavaScript file inclusions.
28
     *
29
     * @var array
30
     */
31
    private $_jsfiles = [];
32
33
    /**
34
     * Array with all prepend JavaScript declarations for the page's head.
35
     *
36
     * @var array
37
     */
38
    private $_prepend_jshead = [];
39
40
    /**
41
     * Boolean showing if jQuery is enabled
42
     *
43
     * @var boolean
44
     */
45
    private $_jquery_enabled = false;
46
47
    private $_jquery_init_scripts = '';
48
49
    /**
50
     * Array with all JQuery state scripts for the page's head.
51
     *
52
     * @var array
53
     */
54
    private $_jquery_states = [];
55
56
    /**
57
     * Array with all linked URLs for HEAD.
58
     *
59
     * @var Array
60
     */
61
    private $_linkhrefs = [];
62
63
    /**
64
     * Array with all methods for the BODY's onload event.
65
     *
66
     * @var Array
67
     */
68
    private $_jsonload = [];
69
70
    /**
71
     * string with all metatags to go into the page head.
72
     *
73
     * @var string
74
     */
75
    private $_meta_head = '';
76
77
    /**
78
     * string with all object tags to go into a page's head.
79
     *
80
     * @var string
81
     */
82
    private $_object_head = '';
83
84
    /**
85
     * String with all css styles to go into a page's head.
86
     *
87
     * @var string
88
     */
89
    private $_style_head = '';
90
91
    /**
92
     * Array with all link elements to be included in a page's head.
93
     *
94
     * @var array
95
     */
96
    private $_link_head = [];
97
98
    const HEAD_PLACEHOLDER = '<!-- MIDCOM_HEAD_ELEMENTS -->';
99
100
    private static $listener_added = false;
101
102
    /**
103
     * Sets the page title for the current context.
104
     *
105
     * This can be retrieved by accessing the component context key
106
     * MIDCOM_CONTEXT_PAGETITLE.
107
     *
108
     * @param string $string    The title to set.
109
     */
110 135
    public function set_pagetitle($string)
111
    {
112 135
        midcom_core_context::get()->set_key(MIDCOM_CONTEXT_PAGETITLE, $string);
113 135
    }
114
115
    /**
116
     * Register JavaScript File for referring in the page.
117
     *
118
     * This allows MidCOM components to register JavaScript code
119
     * during page processing. The site style code can then query this queued-up code
120
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
121
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
122
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
123
     * BODY-tag. Note, that these suggestions are not enforced, if you want a JScript
124
     * clean site, just omit the print calls and you should be fine in almost all
125
     * cases.
126
     *
127
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
128
     *
129
     * @param string $url    The URL to the file to-be referenced.
130
     * @param boolean $prepend Whether to add the JS include to beginning of includes
131
     * @see add_jscript()
132
     * @see add_jsonload()
133
     * @see print_head_elements()
134
     * @see print_jsonload()
135
     */
136 247
    public function add_jsfile($url, $prepend = false)
137
    {
138
        // Adds a URL for a <script type="text/javascript" src="tinymce.js"></script>
139
        // like call. $url is inserted into src. Duplicates are omitted.
140 247
        if (!in_array($url, $this->_jsfiles)) {
141 32
            $this->_jsfiles[] = $url;
142 32
            $js_call = ['url' => $url];
143 32
            if ($prepend) {
144
                // Add the javascript include to the beginning, not the end of array
145
                array_unshift($this->_jshead, $js_call);
146
            } else {
147 32
                $this->_jshead[] = $js_call;
148
            }
149
        }
150 247
    }
151
152
    /**
153
     * Register JavaScript Code for output directly in the page.
154
     *
155
     * This allows components to register JavaScript code
156
     * during page processing. The site style can then query this queued-up code
157
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
158
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
159
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
160
     * BODY-tag. Note, that these suggestions are not enforced
161
     *
162
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
163
     *
164
     * @param string $script    The code to be included directly in the page.
165
     * @see add_jsfile()
166
     * @see add_jsonload()
167
     * @see print_head_elements()
168
     * @see print_jsonload()
169
     */
170 29
    public function add_jscript($script, $defer = '', $prepend = false)
171
    {
172 29
        $js_call = ['content' => trim($script), 'defer' => $defer];
173 29
        if ($prepend) {
174
            $this->_prepend_jshead[] = $js_call;
175
        } else {
176 29
            $this->_jshead[] = $js_call;
177
        }
178 29
    }
179
180
    /**
181
     * Register JavaScript snippets to jQuery states.
182
     *
183
     * This allows components to register JavaScript code to the jQuery states.
184
     * Possible ready states: document.ready
185
     *
186
     * @param string $script    The code to be included in the state.
187
     * @param string $state    The state where to include the code to. Defaults to document.ready
188
     * @see print_jquery_statuses()
189
     */
190 3
    public function add_jquery_state_script($script, $state = 'document.ready')
191
    {
192 3
        $js_call = "\n" . trim($script) . "\n";
193
194 3
        if (!isset($this->_jquery_states[$state])) {
195 1
            $this->_jquery_states[$state] = $js_call;
196
        } else {
197 2
            $this->_jquery_states[$state] .= $js_call;
198
        }
199 3
    }
200
201
    /**
202
     * Register some object tags to be added to the head element.
203
     *
204
     * This allows components to register object tags to be placed in the
205
     * head section of the page.
206
     *
207
     * @param  string $script    The input between the <object></object> tags.
208
     * @param  array  $attributes Array of attribute=> value pairs to be placed in the tag.
209
     * @see print_head_elements()
210
     */
211
    public function add_object_head($script, $attributes = null)
212
    {
213
        $this->_object_head .= '<object' . $this->_get_attribute_string($attributes) . '>' . $script . "</object>\n";
214
    }
215
216
    /**
217
     *  Register a metatag to be added to the head element.
218
     *  This allows components to register metatags to be placed in the
219
     *  head section of the page.
220
     *
221
     *  @param  array  $attributes Array of attribute => value pairs to be placed in the tag.
222
     *  @see print_head_elements()
223
     */
224
    public function add_meta_head($attributes = null)
225
    {
226
        $this->_meta_head .= '<meta' . $this->_get_attribute_string($attributes) . ' />' . "\n";
227
    }
228
229
    /**
230
     * Register a styleblock / style link  to be added to the head element.
231
     * This allows components to register extra CSS sheets they wants to include.
232
     * in the head section of the page.
233
     *
234
     * @param  string $script    The input between the <style></style> tags.
235
     * @param  array  $attributes Array of attribute=> value pairs to be placed in the tag.
236
     * @see print_head_elements()
237
     */
238
    public function add_style_head($script, $attributes = null)
239
    {
240
        $this->_style_head .= '<style type="text/css"' . $this->_get_attribute_string($attributes) . '>' . $script . "</style>\n";
241
    }
242
243
    private function _get_attribute_string($attributes) : string
244
    {
245
        $string = '';
246
        if (null === $attributes) {
247
            return $string;
248
        }
249
        foreach ($attributes as $key => $val) {
250
            $string .= ' ' . $key . '="' . htmlspecialchars($val, ENT_COMPAT) . '"';
251
        }
252
        return $string;
253
    }
254
255
    /**
256
     * Register a link element to be placed in the page head.
257
     *
258
     * This allows components to register extra CSS links.
259
     * Example to use this to include a CSS link:
260
     * <code>
261
     * $attributes = array ('rel' => 'stylesheet',
262
     *                      'type' => 'text/css',
263
     *                      'href' => '/style.css'
264
     *                      );
265
     * midcom::get()->head->add_link_head($attributes);
266
     * </code>
267
     *
268
     * Each URL will only be added once. When trying to add the same URL a second time,
269
     * it will be moved to the end of the stack, so that CSS overrides behave as the developer
270
     * intended
271
     *
272
     * @param  array $attributes Array of attribute => value pairs to be placed in the tag.
273
     * @see print_head_elements()
274
     */
275 289
    public function add_link_head(array $attributes)
276
    {
277 289
        if (!array_key_exists('href', $attributes)) {
278
            return false;
279
        }
280
281
        // Register each URL only once
282 289
        if (($key = array_search($attributes['href'], $this->_linkhrefs)) !== false) {
283 282
            if (end($this->_linkhrefs) != $attributes['href']) {
284 257
                unset($this->_linkhrefs[$key]);
285 257
                $this->_linkhrefs[] = $attributes['href'];
286 257
                reset($this->_linkhrefs);
287
            }
288 282
            return false;
289
        }
290
291 35
        $this->_linkhrefs[] = $attributes['href'];
292 35
        $this->_link_head[$attributes['href']] = $attributes;
293 35
    }
294
295
    /**
296
     * Convenience shortcut for adding CSS files
297
     *
298
     * @param string $url The stylesheet URL
299
     * @param string $media The media type(s) for the stylesheet, if any
300
     */
301 282
    public function add_stylesheet($url, $media = false)
302
    {
303
        $attributes = [
304 282
            'rel'  => 'stylesheet',
305 282
            'type' => 'text/css',
306 282
            'href' => $url,
307
        ];
308 282
        if ($media) {
309 32
            $attributes['media'] = $media;
310
        }
311 282
        $this->add_link_head($attributes);
312 282
    }
313
314
    /**
315
     * Register a JavaScript method for the body onload event
316
     *
317
     * This allows components to register JavaScript code
318
     * during page processing. The site style can then query this queued-up code
319
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
320
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
321
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
322
     * BODY-tag. Note that these suggestions are not enforced.
323
     *
324
     * @param string $method    The name of the method to be called on page startup, including parameters but excluding the ';'.
325
     * @see add_jsfile()
326
     * @see add_jscript()
327
     * @see print_head_elements()
328
     * @see print_jsonload()
329
     */
330
    public function add_jsonload($method)
331
    {
332
        // Adds a method name for <body onload=".."> The string must not end with a ;, it is added automagically
333
        $this->_jsonload[] = $method;
334
    }
335
336
    /**
337
     * Echo the registered javascript code.
338
     *
339
     * This allows components to register JavaScript code
340
     * during page processing. The site style code can then query this queued-up code
341
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
342
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
343
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
344
     * BODY-tag. Note, that these suggestions are not enforced
345
     *
346
     * The sequence of the add_jsfile and add_jscript commands is kept stable.
347
     *
348
     * This is usually called during the BODY region of your style:
349
     *
350
     * <code>
351
     * <html>
352
     *     <body <?php midcom::get()->head->print_jsonload();?>>
353
     *            <!-- your actual body -->
354
     *     </body>
355
     * </html>
356
     * </code>
357
     *
358
     * @see add_jsfile()
359
     * @see add_jscript()
360
     * @see add_jsonload()
361
     * @see print_head_elements()
362
     */
363 25
    public function print_jsonload()
364
    {
365 25
        if (!empty($this->_jsonload)) {
366
            $calls = implode("; ", $this->_jsonload);
367
            echo " onload=\"$calls\" ";
368
        }
369 25
    }
370
371
    /**
372
     * Marks where the _head elements added should be rendered.
373
     *
374
     * Place the method within the <head> section of your page.
375
     *
376
     * This allows components to register HEAD elements
377
     * during page processing. The site style can then query this queued-up code
378
     * at anytime it likes. The queue-up SHOULD be done during the code-init phase,
379
     * while the print_head_elements output SHOULD be included in the HTML HEAD area and
380
     * the HTTP onload attribute returned by print_jsonload SHOULD be included in the
381
     * BODY tag. Note that these suggestions are not enforced
382
     *
383
     * @see add_link_head()
384
     * @see add_object_head()
385
     * @see add_style_head()
386
     * @see add_meta_head()
387
     * @see add_jsfile()
388
     * @see add_jscript()
389
     */
390 28
    public function print_head_elements()
391
    {
392 28
        if (!self::$listener_added) {
393 1
            midcom::get()->dispatcher->addListener(KernelEvents::RESPONSE, [$this, 'inject_head_elements']);
394 1
            self::$listener_added = true;
395
        }
396 28
        echo self::HEAD_PLACEHOLDER;
397 28
    }
398
399
    /**
400
     * This function renders the elements added by the various add methods
401
     * and injects them into the response
402
     *
403
     * @param FilterResponseEvent $event
404
     */
405 335
    public function inject_head_elements(FilterResponseEvent $event)
406
    {
407 335
        if (!$event->isMasterRequest()) {
408 335
            return;
409
        }
410
        $response = $event->getResponse();
411
        $content = $response->getContent();
412
413
        $first = strpos($content, self::HEAD_PLACEHOLDER);
414
        if ($first === false) {
415
            return;
416
        }
417
418
        $head = $this->render();
419
        $new_content = substr_replace($content, $head, $first, strlen(self::HEAD_PLACEHOLDER));
420
        $response->setContent($new_content);
421
        if ($length = $response->headers->get('Content-Length')) {
422
            $delta = strlen($head) - strlen(self::HEAD_PLACEHOLDER);
423
            $response->headers->set('Content-Length', $length + $delta);
424
        }
425
    }
426
427
    public function render() : string
428
    {
429
        $head = $this->_meta_head;
430
        foreach ($this->_linkhrefs as $url) {
431
            $attributes = $this->_link_head[$url];
432
            $is_conditional = false;
433
434
            if (array_key_exists('condition', $attributes)) {
435
                $head .= "<!--[if {$attributes['condition']}]>\n";
436
                $is_conditional = true;
437
                unset($attributes['condition']);
438
            }
439
440
            $head .= "<link" . $this->_get_attribute_string($attributes) . " />\n";
441
442
            if ($is_conditional) {
443
                $head .= "<![endif]-->\n";
444
            }
445
        }
446
447
        $head .= $this->_object_head;
448
        $head .= $this->_style_head;
449
450
        if ($this->_jquery_enabled) {
451
            $head .= $this->_jquery_init_scripts;
452
        }
453
454
        if (!empty($this->_prepend_jshead)) {
455
            $head .= array_reduce($this->_prepend_jshead, [$this, 'render_js'], '');
456
        }
457
458
        $head .= array_reduce($this->_jshead, [$this, 'render_js'], '');
459
        return $head . $this->render_jquery_statuses();
460
    }
461
462 1
    private function render_js(string $carry, array $js_call) : string
463
    {
464 1
        if (array_key_exists('url', $js_call)) {
465 1
            return $carry . '<script type="text/javascript" src="' . $js_call['url'] . "\"></script>\n";
466
        }
467 1
        $carry .= '<script type="text/javascript"' . ($js_call['defer'] ?? '') . ">\n";
468 1
        $carry .= $js_call['content'] . "\n";
469 1
        return $carry . "</script>\n";
470
    }
471
472 6
    public function get_jshead_elements() : array
473
    {
474 6
        return $this->_prepend_jshead + $this->_jshead;
475
    }
476
477
    public function get_link_head() : array
478
    {
479
        return $this->_link_head;
480
    }
481
482
    /**
483
     * Init jQuery
484
     *
485
     * This method adds jQuery support to the page
486
     */
487 247
    public function enable_jquery($version = null)
488
    {
489 247
        if ($this->_jquery_enabled) {
490 246
            return;
491
        }
492
493 1
        if (!$version) {
494 1
            $version = midcom::get()->config->get('jquery_version');
495
        }
496
497 1
        if (midcom::get()->config->get('jquery_load_from_google')) {
498
            // Use Google's hosted jQuery version
499
            $this->_jquery_init_scripts .= $this->render_js("\n", ['url' => 'https://www.google.com/jsapi']);
500
            $this->_jquery_init_scripts .= $this->render_js('', ['content' => 'google.load("jquery", "' . $version . '");']);
501
        } else {
502 1
            $url = MIDCOM_STATIC_URL . "/jQuery/jquery-{$version}.js";
503 1
            $this->_jquery_init_scripts .= $this->render_js("\n", ['url' => $url]);
504
        }
505
506 1
        if (!defined('MIDCOM_JQUERY_UI_URL')) {
507 1
            define('MIDCOM_JQUERY_UI_URL', MIDCOM_STATIC_URL . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version'));
508
        }
509
510 1
        $script  = "const MIDCOM_STATIC_URL = '" . MIDCOM_STATIC_URL . "',\n";
511 1
        $script .= "      MIDCOM_PAGE_PREFIX = '" . midcom_connection::get_url('self') . "';\n";
512
513 1
        $this->_jquery_init_scripts .= $this->render_js('', ['content' => trim($script)]);
514
515 1
        $this->_jquery_enabled = true;
516 1
    }
517
518
    /**
519
     * Renders the scripts added by the add_jquery_state_script method.
520
     *
521
     * This method is called from print_head_elements method.
522
     *
523
     * @see add_jquery_state_script()
524
     * @see print_head_elements()
525
     */
526
    private function render_jquery_statuses() : string
527
    {
528
        if (empty($this->_jquery_states)) {
529
            return '';
530
        }
531
532
        $content = '';
533
        foreach ($this->_jquery_states as $status => $scripts) {
534
            list($status_target, $status_method) = explode('.', $status);
535
            $content .= "jQuery({$status_target}).{$status_method}(function() {\n";
536
            $content .= $scripts . "\n";
537
            $content .= "});\n";
538
        }
539
540
        return $this->render_js('', ['content' => $content]);
541
    }
542
543
    /**
544
     * Add jquery ui components
545
     *
546
     * core and widget are loaded automatically. Also loads jquery.ui theme,
547
     * either the configured theme one or a hardcoded default (base theme)
548
     *
549
     * @param array $components The components that should be loaded
550
     */
551 224
    public function enable_jquery_ui(array $components = [])
552
    {
553 224
        $this->enable_jquery();
554 224
        $this->add_jsfile(MIDCOM_JQUERY_UI_URL . '/core.min.js');
555
556 224
        foreach ($components as $component) {
557 222
            $path = $component;
558 222
            if (strpos($component, 'effect') === 0) {
559
                if ($component !== 'effect') {
560
                    $path = 'effects/' . $component;
561
                }
562
            } else {
563 222
                $path = 'widgets/' . $component;
564
            }
565
566 222
            $this->add_jsfile(MIDCOM_JQUERY_UI_URL . '/' . $path . '.min.js');
567
568 222
            if ($component == 'datepicker') {
569 36
                $lang = midcom::get()->i18n->get_current_language();
570
                /*
571
                 * The calendar doesn't have all lang files and some are named differently
572
                 * Since a missing lang file causes the calendar to break, let's make extra sure
573
                 * that this won't happen
574
                 */
575 36
                if (!file_exists(MIDCOM_STATIC_ROOT . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version') . "/i18n/datepicker-{$lang}.min.js")) {
576 36
                    $lang = midcom::get()->i18n->get_fallback_language();
577 36
                    if (!file_exists(MIDCOM_STATIC_ROOT . "/jQuery/jquery-ui-" . midcom::get()->config->get('jquery_ui_version') . "/i18n/datepicker-{$lang}.min.js")) {
578 36
                        $lang = null;
579
                    }
580
                }
581
582 36
                if ($lang) {
583
                    $this->add_jsfile(MIDCOM_JQUERY_UI_URL . "/i18n/datepicker-{$lang}.min.js");
584
                }
585
            }
586
        }
587
588 224
        $this->add_stylesheet(MIDCOM_STATIC_URL . '/jQuery/jquery-ui-1.12.icon-font.min.css');
589 224
        if (midcom::get()->config->get('jquery_ui_theme')) {
590
            $this->add_stylesheet(midcom::get()->config->get('jquery_ui_theme'));
591
        } else {
592 224
            $this->add_stylesheet(MIDCOM_JQUERY_UI_URL . '/themes/base/jquery-ui.min.css');
593
        }
594 224
    }
595
}
596