PluginProbe
Customify / trunk
Customify vtrunk
2.10.9 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.7.1 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.6.0 1.6.0.1 1.6.5 1.7.0 1.7.1 All 77 releases
customify / includes / class-customify-block-editor.php

class-customify-block-editor.php in Customify trunk, at includes/class-customify-block-editor.php

597 lines 17.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * This is the class that handles the overall logic for integration with the new Gutenberg Editor (WordPress 5.0+).
4 *
5 * @see https://pixelgrade.com
6 * @author Pixelgrade
7 * @since 2.2.0
8 */
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly
12 }
13
14 if ( ! class_exists( 'Customify_Block_Editor' ) ) {
15
16 class Customify_Block_Editor {
17
18 /**
19 * Holds the only instance of this class.
20 * @var null|Customify_Block_Editor
21 * @access protected
22 * @since 2.2.0
23 */
24 protected static $_instance = null;
25
26 /*
27 * Selectors that we will use to constrain CSS rules to certain scopes.
28 */
29 public static $editor_namespace_selector = '.editor-styles-wrapper';
30 public static $title_namespace_selector = '.editor-styles-wrapper .editor-post-title';
31 public static $title_input_namespace_selector = '.editor-styles-wrapper .editor-post-title';
32 public static function get_block_namespace_selector() {
33 global $wp_version;
34
35 $is_old_wp_version = version_compare($wp_version, '5.4', '<');
36
37 if( $is_old_wp_version ) {
38 return '.editor-styles-wrapper .editor-block-list__block';
39 }
40
41 return '.editor-styles-wrapper .block-editor-block-list__block';
42 }
43
44 /**
45 * Regexes
46 */
47 public static $gutenbergy_selector_regex = '/^(\.edit-post-visual-editor|\.editor-visual-editor|\.editor-styles-wrapper|\.editor-block-list__block).*$/';
48 public static $root_regex = '/^(body|html).*$/';
49 public static $title_regex = '/^(h1|h1\s+.*|\.single\s*\.entry-title.*|\.entry-title.*|\.page-title.*|\.article__?title.*)$/';
50 /* Regexes based on which we will ignore selectors = do not include them in the selector list for a certain rule. */
51 public static $excluded_selectors_regex = array(
52 // We don't want to mess with buttons as we have a high likelihood of messing with the Gutenberg toolbar.
53 '/^\s*button/',
54 '/^\s*\.button/',
55 '/^\s*input/',
56 '/^\s*select/',
57 '/^\s*#/', // ignore all ids
58 '/^\s*div#/', // ignore all ids
59
60 '/\.u-/',
61 '/\.c-/',
62 '/\.o-/',
63 '/\.site-/',
64 '/\.card/',
65
66 '/^\s*\.archive/',
67 '/^\s*\.search/',
68 '/^\s*\.no-results/',
69 '/^\s*\.home/',
70 '/^\s*\.blog/',
71 '/^\s*\.site-/',
72 '/\.search/',
73 '/\.page/',
74 '/\.mce-content-body/',
75 '/\.attachment/',
76 '/\.mobile/',
77
78 '/\.sticky/',
79 '/\.custom-logo-link/',
80
81 '/\.entry-meta/',
82 '/\.entry-footer/',
83 '/\.header-meta/',
84 '/\.nav/',
85 '/\.main-navigation/',
86 '/navbar/',
87 '/comment/',
88 '/\.dummy/',
89 '/\.back-to-top/',
90 '/\.page-numbers/',
91 '/\.featured/',
92 '/\.widget/',
93 '/\.edit-link/',
94 '/\.posted-on/',
95 '/\.cat-links/',
96 '/\.posted-by/',
97 '/\.more-link/',
98
99 '/jetpack/',
100 '/wpforms/',
101 '/contact-form/',
102 '/sharedaddy/',
103 );
104
105 /**
106 * Constructor.
107 *
108 * @since 2.2.0
109 */
110 protected function __construct() {
111 $this->init();
112 }
113
114 /**
115 * Initialize this module.
116 *
117 * @since 2.2.0
118 */
119 public function init() {
120
121 // Hook up.
122 $this->add_hooks();
123 }
124
125 /**
126 * Initiate our hooks
127 *
128 * @since 2.2.0
129 */
130 public function add_hooks() {
131
132 // Styles and scripts when editing.
133 add_action( 'enqueue_block_assets', array( $this, 'dynamic_styles_scripts' ), 999 );
134
135 // Styles on the front end.
136 add_action( 'enqueue_block_assets', array( $this, 'frontend_styles' ), 999 );
137
138 add_action( 'admin_init', array( $this, 'editor_color_palettes' ), 20 );
139 }
140
141 /**
142 * Determine if Gutenberg is supported.
143 *
144 * @return bool
145 * @since 2.2.0
146 *
147 */
148 public function is_supported() {
149 $gutenberg = false;
150
151 // Determine if the block editor is active for the frontend.
152 if ( has_action( 'enqueue_block_assets' ) ) {
153 // Gutenberg is installed and activated.
154 $gutenberg = true;
155 }
156
157 // Determine if the block editor is being used in the WP admin.
158 $current_screen = get_current_screen();
159 if ( is_admin() && method_exists( $current_screen, 'is_block_editor' ) && get_current_screen()->is_block_editor() ) {
160 $gutenberg = true;
161 }
162
163 return apply_filters( 'customify_gutenberg_is_supported', $gutenberg );
164 }
165
166 public function get_editor_style_handle() {
167 global $wp_styles;
168 if ( ! ( $wp_styles instanceof WP_Styles ) ) {
169 return '';
170 }
171
172 // We need to look into the registered theme stylesheets and get the one most likely to be used for Gutenberg.
173 // Thus we can attach inline styles to it.
174 $theme_dir_uri = get_template_directory_uri();
175 $theme_slug = get_template();
176
177 $handle = 'wp-edit-post'; // this is better than nothing as it is the main editor style.
178 $reversed = array_reverse( $wp_styles->registered );
179 /** @var _WP_Dependency $style */
180 foreach ( $reversed as $style ) {
181 // This is the most precise.
182 if ( 0 === strpos( $style->src, $theme_dir_uri ) ) {
183 $handle = $style->handle;
184 break;
185 }
186
187 // If it is prefixed with the theme slug, it is good also.
188 if ( 0 === strpos( $style->handle, $theme_slug . '-' ) || 0 === strpos( $style->handle, $theme_slug . '_' ) ) {
189 $handle = $style->handle;
190 break;
191 }
192 }
193
194 return $handle;
195 }
196
197 public function get_frontend_style_handle() {
198 global $wp_styles;
199 if ( ! ( $wp_styles instanceof WP_Styles ) ) {
200 return '';
201 }
202
203 // We need to look into the registered theme stylesheets and get the one most likely to be used for Gutenberg.
204 // Thus we can attach inline styles to it.
205 $style_css_uri = get_template_directory_uri() . '/style.css';
206 $theme_slug = get_template();
207
208 $handle = 'wp-block-library'; // this is better than nothing as it is the main editor frontend style.
209 $reversed = array_reverse( $wp_styles->registered );
210 /** @var _WP_Dependency $style */
211 foreach ( $reversed as $style ) {
212 // This is the most precise.
213 if ( 0 === strpos( $style->src, $style_css_uri ) ) {
214 $handle = $style->handle;
215 break;
216 }
217
218 // If it is prefixed with the theme slug, it is good also.
219 if ( ( 0 === strpos( $style->handle, $theme_slug . '-' ) || 0 === strpos( $style->handle, $theme_slug . '_' ) )
220 && false !== strpos( $style->src, '.css' ) ) {
221 $handle = $style->handle;
222 break;
223 }
224 }
225
226 return $handle;
227 }
228
229 /**
230 * Output Customify's dynamic styles and scripts in the Gutenberg context.
231 *
232 * @since 2.2.0
233 */
234 public function dynamic_styles_scripts() {
235 if ( ! $this->is_admin_block_editor_screen() ) {
236 return;
237 }
238
239 if ( ! PixCustomifyPlugin()->settings->get_plugin_setting( 'enable_editor_style', true ) ) {
240 return;
241 }
242
243 require_once( PixCustomifyPlugin()->get_base_path() . 'includes/class-customify-fonts-global.php' );
244
245 $enqueue_parent_handle = PixCustomifyPlugin()->get_slug() . '-editor-dynamic';
246 wp_register_style( $enqueue_parent_handle, false, array(), PixCustomifyPlugin()->get_version() );
247 wp_enqueue_style( $enqueue_parent_handle );
248
249 wp_register_script(
250 PixCustomifyPlugin()->get_slug() . '-web-font-loader',
251 plugins_url( 'js/vendor/webfontloader-1-6-28.min.js', PixCustomifyPlugin()->get_file() ),
252 array( 'wp-block-editor' ),
253 PixCustomifyPlugin()->get_version(),
254 false
255 );
256
257 add_filter( 'customify_font_css_selector', array( $this, 'gutenbergify_font_css_selectors' ), 10, 2 );
258 Customify_Fonts_Global::instance()->enqueue_frontend_scripts_styles();
259 wp_add_inline_style( $enqueue_parent_handle, Customify_Fonts_Global::instance()->getFontsDynamicStyle() );
260 remove_filter( 'customify_font_css_selector', array( $this, 'gutenbergify_font_css_selectors' ), 10 );
261
262 add_filter( 'customify_css_selector', array( $this, 'gutenbergify_css_selectors' ), 10, 2 );
263 wp_add_inline_style( $enqueue_parent_handle, PixCustomifyPlugin()->customizer->get_dynamic_style() );
264 remove_filter( 'customify_css_selector', array( $this, 'gutenbergify_css_selectors' ), 10 );
265
266 // Add color palettes classes.
267 wp_add_inline_style( $enqueue_parent_handle, $this->editor_color_palettes_css_classes() );
268 }
269
270 /**
271 * Determine whether the current request is for an admin block editor canvas.
272 *
273 * @return bool
274 */
275 protected function is_admin_block_editor_screen() {
276 if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) {
277 return false;
278 }
279
280 $current_screen = get_current_screen();
281 if ( ! $current_screen ) {
282 return false;
283 }
284
285 if ( method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ) {
286 return true;
287 }
288
289 return in_array( $current_screen->id, array( 'site-editor', 'site-editor-v2' ), true );
290 }
291
292 public function frontend_styles() {
293 $enqueue_parent_handle = $this->get_frontend_style_handle();
294 if ( empty( $enqueue_parent_handle ) ) {
295 return;
296 }
297
298 // Add color palettes classes.
299 wp_add_inline_style( $enqueue_parent_handle, $this->editor_color_palettes_css_classes() );
300 }
301
302 /**
303 * @param string $selectors
304 * @param array $css_property
305 *
306 * @return string
307 */
308 public function gutenbergify_css_selectors( $selectors, $css_property ) {
309
310 // Treat the selector(s) as an array.
311 $selectors = $this->maybeExplodeSelectors( $selectors );
312
313 $new_selectors = array();
314 foreach ( $selectors as $selector ) {
315 // Clean up
316 $selector = trim( $selector );
317
318 // If the selector matches the excluded, skip it.
319 if ( $this->preg_match_any( self::$excluded_selectors_regex, $selector ) ) {
320 continue;
321 }
322
323 // If the selector is already Gutenbergy, we will not do anything to it
324 if ( preg_match( self::$gutenbergy_selector_regex, $selector ) ) {
325 $new_selectors[] = $selector;
326 continue;
327 }
328
329 // We will let :root selectors be
330 if ( ':root' === $selector ) {
331 $new_selectors[] = $selector;
332 continue;
333 }
334
335 // For root html elements, we will not prefix them, but replace them with the block and title namespace.
336 if ( preg_match( self::$root_regex, $selector ) ) {
337 // We will ignore pseudo-selectors
338 if ( preg_match( '/^(body|html)[\:\+]+.*$/', $selector ) ) {
339 continue;
340 }
341
342 // When it comes to background properties applied at the body level, we need to scope to the editor namespace
343 if ( isset( $css_property['property'] ) && 0 === strpos( $css_property['property'], 'background' ) ) {
344 $new_selectors[] = preg_replace( '/^(html body|body|html)/', self::$editor_namespace_selector, $selector );
345 } else {
346 $new_selectors[] = preg_replace( '/^(html body|body|html)/', self::get_block_namespace_selector(), $selector );
347 $new_selectors[] = preg_replace( '/^(html body|body|html)/', self::$title_namespace_selector, $selector );
348 }
349 continue;
350 }
351
352 // If we encounter selectors that seem that they could target the post title,
353 // we will add selectors for the Gutenberg title also.
354 if ( preg_match( self::$title_regex, $selector ) ) {
355 $new_selectors[] = preg_replace( self::$title_regex, self::$title_input_namespace_selector, $selector );
356 }
357
358 $new_selectors[] = self::get_block_namespace_selector() . ' ' . $selector;
359 }
360
361 return implode( ', ', $new_selectors );
362 }
363
364 /**
365 * @param array $selectors An array of standardized, cleaned selectors where the key is the selector and the value is possible details array.
366 *
367 * @return array
368 */
369 public function gutenbergify_font_css_selectors( $selectors ) {
370
371 $new_selectors = array();
372 foreach ( $selectors as $selector => $selector_details ) {
373 // If the selector matches the excluded, skip it.
374 if ( $this->preg_match_any( self::$excluded_selectors_regex, $selector ) ) {
375 continue;
376 }
377
378 // If the selector is already Gutenbergy, we will not do anything to it
379 if ( preg_match( self::$gutenbergy_selector_regex, $selector ) ) {
380 $new_selectors[ $selector ] = $selector_details;
381 continue;
382 }
383
384 // We will let :root selectors be
385 if ( ':root' === $selector ) {
386 $new_selectors[ $selector ] = $selector_details;
387 continue;
388 }
389
390 // For root html elements, we will not prefix them, but replace them with the block and title namespace.
391 if ( preg_match( self::$root_regex, $selector ) ) {
392 $new_selector = preg_replace( '/^(html body|body|html|)/', self::get_block_namespace_selector(), $selector );
393 $new_selectors[ $new_selector ] = $selector_details;
394 $new_selector = preg_replace( '/^(html body|body|html)/', self::$title_namespace_selector, $selector );
395 $new_selectors[ $new_selector ] = $selector_details;
396 continue;
397 }
398
399 // If we encounter selectors that seem that they could target the post title,
400 // we will add selectors for the Gutenberg title also.
401 if ( preg_match( self::$title_regex, $selector ) ) {
402 $new_selector = preg_replace( self::$title_regex, self::$title_input_namespace_selector, $selector );
403 $new_selectors[ $new_selector ] = $selector_details;
404 }
405
406 $selector = self::get_block_namespace_selector() . ' ' . $selector;
407 $new_selectors[ $selector ] = $selector_details;
408 }
409
410 return $new_selectors;
411 }
412
413 /**
414 * Preg_match a series of regex against a subject.
415 *
416 * @param string|array $regexes
417 * @param string $subject
418 *
419 * @return bool Returns true if at least one of the regex matches, false otherwise.
420 */
421 public function preg_match_any( $regexes, $subject ) {
422 if ( is_string( $regexes ) ) {
423 $regexes = array( $regexes );
424 }
425
426 if ( ! is_array( $regexes ) ) {
427 return false;
428 }
429
430 foreach ( $regexes as $regex ) {
431 if ( preg_match( $regex, $subject ) ) {
432 return true;
433 }
434 }
435
436 return false;
437 }
438
439 /**
440 * Attempt to split a string with selectors and return the parts as an array.
441 * If not a string or no comma present, just returns the value.
442 *
443 * @param mixed $value
444 *
445 * @return array|false|string[]
446 */
447 public function maybeExplodeSelectors( $value ) {
448 if ( ! is_string( $value ) ) {
449 return $value;
450 }
451
452 return preg_split( '#[\s]*,[\s]*#', $value, - 1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
453 }
454
455 /**
456 * Add the SM Color Palettes to the editor sidebar.
457 */
458 public function editor_color_palettes() {
459 // Bail if Color Palettes are not supported
460 if ( ! Customify_Color_Palettes::instance()->is_supported() ) {
461 return;
462 }
463
464 $options_details = PixCustomifyPlugin()->get_options_configs();
465
466 $master_color_control_ids = Customify_Color_Palettes::instance()->get_all_master_color_controls_ids();
467 if ( empty( $master_color_control_ids ) ) {
468 return;
469 }
470
471 $editor_color_palettes = array();
472 foreach ( $master_color_control_ids as $control_id ) {
473 if ( empty( $options_details[ $control_id ] ) ) {
474 continue;
475 }
476
477 $value = get_option( $control_id . '_final' );
478 if ( empty( $value ) ) {
479 $value = $options_details[ $control_id ][ 'default' ];
480 }
481
482 if ( empty( $value ) ) {
483 continue;
484 }
485
486 $editor_color_palettes[] = array(
487 'name' => $options_details[ $control_id ]['label'],
488 'slug' => $control_id,
489 'color' => esc_html( $value ),
490 );
491 }
492
493 if ( ! empty( $editor_color_palettes ) ) {
494 /**
495 * Custom colors for use in the editor.
496 *
497 * @link https://wordpress.org/gutenberg/handbook/reference/theme-support/
498 */
499 add_theme_support(
500 'editor-color-palette',
501 $editor_color_palettes
502 );
503 }
504 }
505
506 /**
507 * Generate the special classes for our colors.
508 */
509 public function editor_color_palettes_css_classes() {
510 // Bail if Color Palettes are not supported
511 if ( ! Customify_Color_Palettes::instance()->is_supported() ) {
512 return '';
513 }
514
515 $options_details = PixCustomifyPlugin()->get_options_configs();
516
517 $master_color_control_ids = Customify_Color_Palettes::instance()->get_all_master_color_controls_ids();
518 if ( empty( $master_color_control_ids ) ) {
519 return '';
520 }
521
522 // Build styles.
523 $css = '';
524 foreach ( $master_color_control_ids as $control_id ) {
525 if ( empty( $options_details[ $control_id ] ) ) {
526 continue;
527 }
528
529 $value = get_option( $control_id . '_final' );
530 if ( empty( $value ) ) {
531 continue;
532 }
533
534 $editor_color_palettes[] = array(
535 'name' => $options_details[ $control_id ]['label'],
536 'slug' => $control_id,
537 'color' => esc_html( $value ),
538 );
539
540 $color_in_kebab_case = self::to_kebab_case( $control_id );
541
542 $css .= '.has-' . $color_in_kebab_case . '-color { color: ' . esc_attr( $value ) . ' !important; }';
543 $css .= '.has-' . $color_in_kebab_case . '-background-color { background-color: ' . esc_attr( $value ) . '; }';
544 }
545
546 return wp_strip_all_tags( $css );
547 }
548
549 public static function to_kebab_case( $string ) {
550 $parts = preg_split( "/[\n\r\t -_]+/", preg_replace( "/['\x{2019}]/u", '', $string ), - 1, PREG_SPLIT_NO_EMPTY );
551 if ( ! is_array( $parts ) ) {
552 return '';
553 }
554 return implode( '-', array_map( '\strtolower', $parts ) );
555 }
556
557 /**
558 * Main Customify_Block_Editor Instance
559 *
560 * Ensures only one instance of Customify_Block_Editor is loaded or can be loaded.
561 *
562 * @return Customify_Block_Editor Main Customify_Block_Editor instance
563 * @since 2.2.0
564 * @static
565 *
566 */
567 public static function instance() {
568
569 if ( is_null( self::$_instance ) ) {
570 self::$_instance = new self();
571 }
572
573 return self::$_instance;
574 }
575
576 /**
577 * Cloning is forbidden.
578 *
579 * @since 2.2.0
580 */
581 public function __clone() {
582
583 _doing_it_wrong( __FUNCTION__, esc_html__( 'You should not do that!', 'customify' ), null );
584 }
585
586 /**
587 * Unserializing instances of this class is forbidden.
588 *
589 * @since 2.2.0
590 */
591 public function __wakeup() {
592
593 _doing_it_wrong( __FUNCTION__, esc_html__( 'You should not do that!', 'customify' ), null );
594 }
595 }
596 }
597