Passed
Push — develop ( 725b0b...952465 )
by Aristeides
09:10 queued 04:22
created

postmessage/class-kirki-modules-postmessage.php (2 issues)

1
<?php
2
/**
3
 * Automatic postMessage scripts calculation for Kirki controls.
4
 *
5
 * @package     Kirki
6
 * @category    Modules
7
 * @author      Aristeides Stathopoulos
8
 * @copyright   Copyright (c) 2017, Aristeides Stathopoulos
9
 * @license    https://opensource.org/licenses/MIT
10
 * @since       3.0.0
11
 */
12
13
// Exit if accessed directly.
14
if ( ! defined( 'ABSPATH' ) ) {
15
	exit;
16
}
17
18
/**
19
 * Adds styles to the customizer.
20
 */
21
class Kirki_Modules_PostMessage {
22
23
	/**
24
	 * The object instance.
25
	 *
26
	 * @static
27
	 * @access private
28
	 * @since 3.0.0
29
	 * @var object
30
	 */
31
	private static $instance;
32
33
	/**
34
	 * The script.
35
	 *
36
	 * @access protected
37
	 * @since 3.0.0
38
	 * @var string
39
	 */
40
	protected $script = '';
41
42
	/**
43
	 * Constructor.
44
	 *
45
	 * @access protected
46
	 * @since 3.0.0
47
	 */
48
	protected function __construct() {
49
		add_action( 'customize_preview_init', array( $this, 'postmessage' ) );
50
	}
51
52
	/**
53
	 * Gets an instance of this object.
54
	 * Prevents duplicate instances which avoid artefacts and improves performance.
55
	 *
56
	 * @static
57
	 * @access public
58
	 * @since 3.0.0
59
	 * @return object
60
	 */
61
	public static function get_instance() {
62
		if ( ! self::$instance ) {
63
			self::$instance = new self();
64
		}
65
		return self::$instance;
66
	}
67
68
	/**
69
	 * Enqueues the postMessage script
70
	 * and adds variables to it using the wp_localize_script function.
71
	 * The rest is handled via JS.
72
	 */
73
	public function postmessage() {
74
		wp_enqueue_script( 'kirki_auto_postmessage', trailingslashit( Kirki::$url ) . 'modules/postmessage/postmessage.js', array( 'jquery', 'customize-preview' ), KIRKI_VERSION, true );
75
		$fields = Kirki::$fields;
76
		foreach ( $fields as $field ) {
77
			if ( isset( $field['transport'] ) && 'postMessage' === $field['transport'] && isset( $field['js_vars'] ) && ! empty( $field['js_vars'] ) && is_array( $field['js_vars'] ) && isset( $field['settings'] ) ) {
78
				$this->script .= $this->script( $field );
79
			}
80
		}
81
		$this->script = apply_filters( 'kirki_postmessage_script', $this->script );
82
		wp_add_inline_script( 'kirki_auto_postmessage', $this->script, 'after' );
83
	}
84
85
	/**
86
	 * Generates script for a single field.
87
	 *
88
	 * @access protected
89
	 * @since 3.0.0
90
	 * @param array $args The arguments.
91
	 */
92
	protected function script( $args ) {
93
		$script = 'wp.customize(\'' . $args['settings'] . '\',function(value){value.bind(function(newval){';
94
95
		$add_css = false;
96
		foreach ( $args['js_vars'] as $js_var ) {
97
			if ( ! isset( $js_var['function'] ) || 'html' !== $js_var['function'] ) {
98
				$add_css = true;
99
			}
100
		}
101
102
		if ( $add_css ) {
103
104
			// append unique style tag if not exist
105
			// The style ID.
106
			$style_id = 'kirki-postmessage-' . str_replace( array( '[', ']' ), '', $args['settings'] );
107
			$script  .= 'if(null===document.getElementById(\'' . $style_id . '\')||\'undefined\'===typeof document.getElementById(\'' . $style_id . '\')){jQuery(\'head\').append(\'<style id="' . $style_id . '"></style>\');}';
108
		}
109
110
		// Add anything we need before the main script.
111
		$script .= $this->before_script( $args );
112
113
		$field = array(
114
			'scripts' => array(),
115
		);
116
117
		// Loop through the js_vars and generate the script.
118
		foreach ( $args['js_vars'] as $key => $js_var ) {
119
120
			// Skip styles if "exclude" is defined and value is excluded.
121
			if ( isset( $js_var['exclude'] ) ) {
122
				$js_var['exclude'] = (array) $js_var['exclude'];
123
				$script           .= 'exclude=false;';
124
				foreach ( $js_var['exclude'] as $exclussion ) {
125
					$script .= "if(newval=='{$exclussion}'||(''==='{$exclussion}'&&_.isObject(newval)&&_.isEmpty(newval))){exclude=true;}";
126
				}
127
			}
128
			if ( isset( $js_var['element'] ) ) {
129
130
				// Array to string.
131
				if ( is_array( $js_var['element'] ) ) {
132
					$js_var['element'] = implode( ',', $js_var['element'] );
133
				}
134
135
				// Replace single quotes with double quotes to avoid issues with the compiled JS.
136
				$js_var['element'] = str_replace( '\'', '"', $js_var['element'] );
137
			}
138
			if ( isset( $js_var['function'] ) && 'html' === $js_var['function'] ) {
139
				$script .= $this->script_html_var( $js_var );
140
				continue;
141
			}
142
			$js_var['index_key'] = $key;
143
			$callback            = $this->get_callback( $args );
144
			if ( is_callable( $callback ) ) {
145
				$field['scripts'][ $key ] = call_user_func_array( $callback, array( $js_var, $args ) );
146
				continue;
147
			}
148
			$field['scripts'][ $key ] = $this->script_var( $js_var );
149
		}
150
		$combo_extra_script = '';
151
		$combo_css_script   = '';
152
		foreach ( $field['scripts'] as $script_array ) {
153
			$combo_extra_script .= $script_array['script'];
154
			$combo_css_script   .= ( 'css' !== $combo_css_script ) ? $script_array['css'] : '';
155
		}
156
		$text = ( 'css' === $combo_css_script ) ? 'css' : '\'' . $combo_css_script . '\'';
157
158
		$script .= $combo_extra_script . "var cssContent={$text};";
159
		if ( isset( $js_var['exclude'] ) ) {
160
			$script .= 'if(true===exclude){cssContent="";}';
161
		}
162
		if ( $add_css ) {
163
			$script .= "jQuery('#{$style_id}').text(cssContent);jQuery('#{$style_id}').appendTo('head');";
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $style_id does not seem to be defined for all execution paths leading up to this point.
Loading history...
164
		}
165
		$script .= '});});';
166
		return $script;
167
	}
168
169
	/**
170
	 * Generates script for a single js_var when using "html" as function.
171
	 *
172
	 * @access protected
173
	 * @since 3.0.0
174
	 * @param array $args  The arguments for this js_var.
175
	 */
176
	protected function script_html_var( $args ) {
177
		$script = ( isset( $args['choice'] ) ) ? "newval=newval['{$args['choice']}'];" : '';
178
179
		// Apply the value_pattern.
180
		if ( isset( $args['value_pattern'] ) && '' !== $args['value_pattern'] ) {
181
			$script .= $this->value_pattern_replacements( 'newval', $args );
182
		}
183
184
		if ( isset( $args['attr'] ) ) {
185
			$script .= "jQuery('{$args['element']}').attr('{$args['attr']}',newval);";
186
			return $script;
187
		}
188
		$script .= "jQuery('{$args['element']}').html(newval);";
189
		return $script;
190
	}
191
192
	/**
193
	 * Generates script for a single js_var.
194
	 *
195
	 * @access protected
196
	 * @since 3.0.0
197
	 * @param array $args  The arguments for this js_var.
198
	 */
199
	protected function script_var( $args ) {
200
		$script          = '';
201
		$property_script = '';
202
203
		$value_key        = 'newval' . $args['index_key'];
204
		$property_script .= $value_key . '=newval;';
205
206
		$args = $this->get_args( $args );
207
208
		// Apply callback to the value if a callback is defined.
209
		if ( ! empty( $args['js_callback'] ) && is_array( $args['js_callback'] ) && isset( $args['js_callback'][0] ) && ! empty( $args['js_callback'][0] ) ) {
210
			$script .= $value_key . '=' . $args['js_callback'][0] . '(' . $value_key . ',' . $args['js_callback'][1] . ');';
211
		}
212
213
		// Apply the value_pattern.
214
		if ( '' !== $args['value_pattern'] ) {
215
			$script .= $this->value_pattern_replacements( $value_key, $args );
216
		}
217
218
		// Tweak to add url() for background-images.
219
		if ( 'background-image' === $args['property'] && ( ! isset( $args['value_pattern'] ) || false === strpos( $args['value_pattern'], 'gradient' ) ) ) {
220
			$script .= 'if(-1===' . $value_key . '.indexOf(\'url(\')){' . $value_key . '=\'url("\'+' . $value_key . '+\'")\';}';
221
		}
222
223
		// Apply prefix.
224
		$value = $value_key;
225
		if ( '' !== $args['prefix'] ) {
226
			$value = "'" . $args['prefix'] . "'+" . $value_key;
227
		}
228
		$css = $args['element'] . '{' . $args['property'] . ':\'+' . $value . '+\'' . $args['units'] . $args['suffix'] . ';}';
229
		if ( isset( $args['media_query'] ) ) {
230
			$css = $args['media_query'] . '{' . $css . '}';
231
		}
232
		return array(
233
			'script' => $property_script . $script,
234
			'css'    => $css,
235
		);
236
	}
237
238
	/**
239
	 * Processes script generation for fields that save an array.
240
	 *
241
	 * @access protected
242
	 * @since 3.0.0
243
	 * @param array $args  The arguments for this js_var.
244
	 */
245
	protected function script_var_array( $args ) {
246
		$script          = ( 0 === $args['index_key'] ) ? 'css=\'\';' : '';
247
		$property_script = '';
248
249
		// Define choice.
250
		$choice = ( isset( $args['choice'] ) && '' !== $args['choice'] ) ? $args['choice'] : '';
251
252
		$value_key        = 'newval' . $args['index_key'];
253
		$property_script .= $value_key . '=newval;';
254
255
		$args = $this->get_args( $args );
256
257
		// Apply callback to the value if a callback is defined.
258
		if ( ! empty( $args['js_callback'] ) && is_array( $args['js_callback'] ) && isset( $args['js_callback'][0] ) && ! empty( $args['js_callback'][0] ) ) {
259
			$script .= $value_key . '=' . $args['js_callback'][0] . '(' . $value_key . ',' . $args['js_callback'][1] . ');';
260
		}
261
		$script .= '_.each(' . $value_key . ', function(subValue,subKey){';
262
263
		// Apply the value_pattern.
264
		if ( '' !== $args['value_pattern'] ) {
265
			$script .= $this->value_pattern_replacements( 'subValue', $args );
266
		}
267
268
		// Tweak to add url() for background-images.
269
		if ( '' === $choice || 'background-image' === $choice ) {
270
			$script .= 'if(\'background-image\'===\'' . $args['property'] . '\'||\'background-image\'===subKey){if(-1===subValue.indexOf(\'url(\')){subValue=\'url("\'+subValue+\'")\';}}';
271
		}
272
273
		// Apply prefix.
274
		$value = $value_key;
0 ignored issues
show
The assignment to $value is dead and can be removed.
Loading history...
275
		if ( '' !== $args['prefix'] ) {
276
			$value = '\'' . $args['prefix'] . '\'+subValue';
277
		}
278
279
		// Mostly used for padding, margin & position properties.
280
		$direction_script  = 'if(_.contains([\'top\',\'bottom\',\'left\',\'right\'],subKey)){';
281
		$direction_script .= 'css+=\'' . $args['element'] . '{' . $args['property'] . '-\'+subKey+\':\'+subValue+\'' . $args['units'] . $args['suffix'] . ';}\';}';
282
283
		// Allows us to apply this just for a specific choice in the array of the values.
284
		if ( '' !== $choice ) {
285
			$choice_is_direction = ( false !== strpos( $choice, 'top' ) || false !== strpos( $choice, 'bottom' ) || false !== strpos( $choice, 'left' ) || false !== strpos( $choice, 'right' ) );
286
287
			// The script.
288
			$script .= 'if(\'' . $choice . '\'===subKey){';
289
			$script .= ( $choice_is_direction ) ? $direction_script . 'else{' : '';
290
			$script .= 'css+=\'' . $args['element'] . '{' . $args['property'] . ':\'+subValue+\';}\';';
291
			$script .= ( $choice_is_direction ) ? '}' : '';
292
			$script .= '}';
293
		} else {
294
295
			// This is where most object-based fields will go.
296
			$script .= $direction_script . 'else{css+=\'' . $args['element'] . '{\'+subKey+\':\'+subValue+\'' . $args['units'] . $args['suffix'] . ';}\';}';
297
		}
298
		$script .= '});';
299
300
		if ( isset( $args['media_query'] ) ) {
301
			$script .= 'css=\'' . $args['media_query'] . '{\'+css+\'}\';';
302
		}
303
304
		return array(
305
			'script' => $property_script . $script,
306
			'css'    => 'css',
307
		);
308
	}
309
310
	/**
311
	 * Processes script generation for typography fields.
312
	 *
313
	 * @access protected
314
	 * @since 3.0.0
315
	 * @param array $args  The arguments for this js_var.
316
	 * @param array $field The field arguments.
317
	 */
318
	protected function script_var_typography( $args, $field ) {
319
		$args = $this->get_args( $args );
320
321
		$script = '';
322
		$css    = '';
323
324
		// Load the font using WenFontloader.
325
		// This is a bit ugly because wp_add_inline_script doesn't allow adding <script> directly.
326
		$webfont_loader = 'sc=\'a\';jQuery(\'head\').append(sc.replace(\'a\',\'<\')+\'script>if(!_.isUndefined(WebFont)&&fontFamily){WebFont.load({google:{families:["\'+fontFamily.replace( /\"/g, \'&quot;\' )+\':\'+variant+\':cyrillic,cyrillic-ext,devanagari,greek,greek-ext,khmer,latin,latin-ext,vietnamese,hebrew,arabic,bengali,gujarati,tamil,telugu,thai"]}});}\'+sc.replace(\'a\',\'<\')+\'/script>\');';
327
328
		// Add the css.
329
		$css_build_array  = array(
330
			'font-family'     => 'fontFamily',
331
			'font-size'       => 'fontSize',
332
			'line-height'     => 'lineHeight',
333
			'letter-spacing'  => 'letterSpacing',
334
			'word-spacing'    => 'wordSpacing',
335
			'text-align'      => 'textAlign',
336
			'text-transform'  => 'textTransform',
337
			'text-decoration' => 'textDecoration',
338
			'color'           => 'color',
339
			'font-weight'     => 'fontWeight',
340
			'font-style'      => 'fontStyle',
341
		);
342
		$choice_condition = ( isset( $args['choice'] ) && '' !== $args['choice'] && isset( $css_build_array[ $args['choice'] ] ) );
343
		$script          .= ( ! $choice_condition ) ? $webfont_loader : '';
344
		foreach ( $css_build_array as $property => $var ) {
345
			if ( $choice_condition && $property !== $args['choice'] ) {
346
				continue;
347
			}
348
			// Fixes https://github.com/aristath/kirki/issues/1436.
349
			if ( ! isset( $field['default'] ) || (
350
				( 'font-family' === $property && ! isset( $field['default']['font-family'] ) ) ||
351
				( 'font-size' === $property && ! isset( $field['default']['font-size'] ) ) ||
352
				( 'line-height' === $property && ! isset( $field['default']['line-height'] ) ) ||
353
				( 'letter-spacing' === $property && ! isset( $field['default']['letter-spacing'] ) ) ||
354
				( 'word-spacing' === $property && ! isset( $field['default']['word-spacing'] ) ) ||
355
				( 'text-align' === $property && ! isset( $field['default']['text-align'] ) ) ||
356
				( 'text-transform' === $property && ! isset( $field['default']['text-transform'] ) ) ||
357
				( 'text-decoration' === $property && ! isset( $field['default']['text-decoration'] ) ) ||
358
				( 'color' === $property && ! isset( $field['default']['color'] ) ) ||
359
				( 'font-weight' === $property && ! isset( $field['default']['variant'] ) && ! isset( $field['default']['font-weight'] ) ) ||
360
				( 'font-style' === $property && ! isset( $field['default']['variant'] ) && ! isset( $field['default']['font-style'] ) )
361
				) ) {
362
				continue;
363
			}
364
			$script .= ( $choice_condition && 'font-family' === $args['choice'] ) ? $webfont_loader : '';
365
366
			if ( 'font-family' === $property || ( isset( $args['choice'] ) && 'font-family' === $args['choice'] ) ) {
367
				$css .= 'fontFamilyCSS=fontFamily;if(0<fontFamily.indexOf(\' \')&&-1===fontFamily.indexOf(\'"\')){fontFamilyCSS=\'"\'+fontFamily+\'"\';}';
368
				$var  = 'fontFamilyCSS';
369
			}
370
			$var  = ( ( empty( $args['prefix'] ) ) ? '' : '\'' . $args['prefix'] . '\'+' ) . $var . ( ( empty( $args['units'] ) ) ? '' : '+\'' . $args['units'] . '\'' ) . ( ( empty( $args['suffix'] ) ) ? '' : '+\'' . $args['suffix'] . '\'' );
371
			$css .= 'css+=(\'\'!==' . $var . ')?\'' . $args['element'] . '\'+\'{' . $property . ':\'+' . $var . '+\';}\':\'\';';
372
		}
373
374
		$script .= $css;
375
		if ( isset( $args['media_query'] ) ) {
376
			$script .= 'css=\'' . $args['media_query'] . '{\'+css+\'}\';';
377
		}
378
		return array(
379
			'script' => $script,
380
			'css'    => 'css',
381
		);
382
	}
383
384
	/**
385
	 * Processes script generation for typography fields.
386
	 *
387
	 * @access protected
388
	 * @since 3.0.0
389
	 * @param array $args  The arguments for this js_var.
390
	 */
391
	protected function script_var_image( $args ) {
392
		$return = $this->script_var( $args );
393
		return array(
394
			'script' => 'newval=(!_.isUndefined(newval.url))?newval.url:newval;' . $return['script'],
395
			'css'    => $return['css'],
396
		);
397
	}
398
399
	/**
400
	 * Adds anything we need before the main script.
401
	 *
402
	 * @access private
403
	 * @since 3.0.0
404
	 * @param array $args The field args.
405
	 * @return string
406
	 */
407
	private function before_script( $args ) {
408
		$script = '';
409
		if ( isset( $args['type'] ) ) {
410
			switch ( $args['type'] ) {
411
				case 'kirki-typography':
412
					$script .= 'fontFamily=(_.isUndefined(newval[\'font-family\']))?\'\':newval[\'font-family\'];variant=(_.isUndefined(newval.variant))?\'400\':newval.variant;fontSize=(_.isUndefined(newval[\'font-size\']))?\'\':newval[\'font-size\'];lineHeight=(_.isUndefined(newval[\'line-height\']))?\'\':newval[\'line-height\'];letterSpacing=(_.isUndefined(newval[\'letter-spacing\']))?\'\':newval[\'letter-spacing\'];wordSpacing=(_.isUndefined(newval[\'word-spacing\']))?\'\':newval[\'word-spacing\'];textAlign=(_.isUndefined(newval[\'text-align\']))?\'\':newval[\'text-align\'];textTransform=(_.isUndefined(newval[\'text-transform\']))?\'\':newval[\'text-transform\'];textDecoration=(_.isUndefined(newval[\'text-decoration\']))?\'\':newval[\'text-decoration\'];color=(_.isUndefined(newval.color))?\'\':newval.color;fw=(!_.isString(newval.variant))?\'400\':newval.variant.match(/\d/g);fontWeight=(!_.isObject(fw))?400:fw.join(\'\');fontStyle=(variant&&-1!==variant.indexOf(\'italic\'))?\'italic\':\'normal\';css=\'\';';
413
					break;
414
			}
415
		}
416
		return $script;
417
	}
418
419
	/**
420
	 * Sanitizes the arguments and makes sure they are all there.
421
	 *
422
	 * @access private
423
	 * @since 3.0.0
424
	 * @param array $args The arguments.
425
	 * @return array
426
	 */
427
	private function get_args( $args ) {
428
429
		// Make sure everything is defined to avoid "undefined index" errors.
430
		$args = wp_parse_args(
431
			$args, array(
432
				'element'       => '',
433
				'property'      => '',
434
				'prefix'        => '',
435
				'suffix'        => '',
436
				'units'         => '',
437
				'js_callback'   => array( '', '' ),
438
				'value_pattern' => '',
439
			)
440
		);
441
442
		// Element should be a string.
443
		if ( is_array( $args['element'] ) ) {
444
			$args['element'] = implode( ',', $args['element'] );
445
		}
446
447
		// Make sure arguments that are passed-on to callbacks are strings.
448
		if ( is_array( $args['js_callback'] ) && isset( $args['js_callback'][1] ) && is_array( $args['js_callback'][1] ) ) {
449
			$args['js_callback'][1] = wp_json_encode( $args['js_callback'][1] );
450
		}
451
452
		if ( ! isset( $args['js_callback'][1] ) ) {
453
			$args['js_callback'][1] = '';
454
		}
455
		return $args;
456
	}
457
458
	/**
459
	 * Returns script for value_pattern & replacements.
460
	 *
461
	 * @access private
462
	 * @since 3.0.0
463
	 * @param string $value   The value placeholder.
464
	 * @param array  $js_vars The js_vars argument.
465
	 * @return string         The script.
466
	 */
467
	private function value_pattern_replacements( $value, $js_vars ) {
468
		$script = '';
469
		$alias  = $value;
470
		if ( ! isset( $js_vars['value_pattern'] ) ) {
471
			return $value;
472
		}
473
		$value = $js_vars['value_pattern'];
474
		if ( isset( $js_vars['pattern_replace'] ) ) {
475
			$script .= 'settings=window.wp.customize.get();';
476
			foreach ( $js_vars['pattern_replace'] as $search => $replace ) {
477
				$replace = '\'+settings["' . $replace . '"]+\'';
478
				$value   = str_replace( $search, $replace, $js_vars['value_pattern'] );
479
				$value   = trim( $value, '+' );
480
			}
481
		}
482
		$value_compiled = str_replace( '$', '\'+' . $alias . '+\'', $value );
483
		$value_compiled = trim( $value_compiled, '+' );
484
485
		return $script . $alias . '=\'' . $value_compiled . '\';';
486
	}
487
488
	/**
489
	 * Get the callback function/method we're going to use for this field.
490
	 *
491
	 * @access private
492
	 * @since 3.0.0
493
	 * @param array $args The field args.
494
	 * @return string|array A callable function or method.
495
	 */
496
	protected function get_callback( $args ) {
497
		switch ( $args['type'] ) {
498
			case 'kirki-background':
499
			case 'kirki-dimensions':
500
			case 'kirki-multicolor':
501
			case 'kirki-sortable':
502
				$callback = array( $this, 'script_var_array' );
503
				break;
504
			case 'kirki-typography':
505
				$callback = array( $this, 'script_var_typography' );
506
				break;
507
			case 'kirki-image':
508
				$callback = array( $this, 'script_var_image' );
509
				break;
510
			default:
511
				$callback = array( $this, 'script_var' );
512
		}
513
		return $callback;
514
	}
515
}
516