PluginProbe
Block Animations, Motion & Scroll Effects – Ghost Kit / 3.7.2
Block Animations, Motion & Scroll Effects – Ghost Kit v3.7.2
3.7.2 3.7.1 3.7.0 3.6.1 trunk 1.6.3 2.25.0 3.3.0 3.3.1 3.3.2 3.3.3 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.6 3.5.0 3.5.1 3.6.0
ghostkit / classes / class-breakpoints.php

class-breakpoints.php in Block Animations, Motion & Scroll Effects – Ghost Kit 3.7.2, at classes/class-breakpoints.php

509 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Breakpoints
4 *
5 * @package ghostkit
6 */
7
8 if ( ! class_exists( 'GhostKit_Breakpoints' ) ) {
9 /**
10 * GhostKit_Breakpoints class
11 */
12 // phpcs:ignore
13 class GhostKit_Breakpoints {
14 /**
15 * Extra Small Default Breakpoint.
16 *
17 * @var int
18 */
19 protected static $default_xs = 576;
20
21 /**
22 * Mobile Default Breakpoint.
23 *
24 * @var int
25 */
26 protected static $default_sm = 768;
27
28 /**
29 * Tablet Breakpoint.
30 *
31 * @var int
32 */
33 protected static $default_md = 992;
34
35 /**
36 * Desktop Breakpoint.
37 *
38 * @var int
39 */
40 protected static $default_lg = 1200;
41
42 /**
43 * Plugin name. Also the directory under uploads that holds the generated CSS.
44 *
45 * @var string
46 */
47 protected $plugin_name = 'ghostkit';
48
49 /**
50 * Plugin version.
51 *
52 * @var string
53 */
54 protected $plugin_version = GHOSTKIT_VERSION;
55
56 /**
57 * GhostKit_Breakpoints constructor.
58 */
59 public function __construct() {
60 add_filter( 'style_loader_src', array( $this, 'change_style_src_to_compile' ), 10, 1 );
61 add_action( 'gkt_before_assets_register', array( $this, 'maybe_generate_css' ) );
62 }
63
64 /**
65 * Get plugin path.
66 *
67 * @return string
68 */
69 public function get_plugin_path() {
70 return ghostkit()->plugin_path;
71 }
72
73 /**
74 * Get plugin url.
75 *
76 * @return string
77 */
78 public function get_plugin_url() {
79 return ghostkit()->plugin_url;
80 }
81
82 /**
83 * Name of the option that holds the hash and the list of generated files.
84 *
85 * @return string
86 */
87 protected function get_option_name() {
88 return str_replace( '-', '_', $this->plugin_name ) . '_breakpoints_css';
89 }
90
91 /**
92 * Directory under uploads that holds the generated CSS.
93 *
94 * @return string
95 */
96 protected function get_output_dir() {
97 $upload_dir = wp_get_upload_dir();
98
99 return $upload_dir['basedir'] . '/' . $this->plugin_name;
100 }
101
102 /**
103 * Stored hash and list of generated files.
104 *
105 * @return array|false
106 */
107 protected function get_generated_css() {
108 $stored = get_option( $this->get_option_name() );
109
110 if (
111 ! is_array( $stored ) ||
112 ! isset( $stored['hash'], $stored['files'] ) ||
113 ! is_array( $stored['files'] )
114 ) {
115 return false;
116 }
117
118 return $stored;
119 }
120
121 /**
122 * Regenerate the CSS when the breakpoints or the plugin version changed.
123 *
124 * @return void
125 */
126 public function maybe_generate_css() {
127 $breakpoints = self::get_breakpoints();
128 $hash = $this->get_breakpoints_hash( $breakpoints );
129 $stored = $this->get_generated_css();
130
131 if ( $hash === $this->get_default_breakpoints_hash() ) {
132 if ( $stored ) {
133 $this->remove_generated_css();
134 }
135 return;
136 }
137
138 if ( $stored && $stored['hash'] === $hash ) {
139 return;
140 }
141
142 $this->remove_generated_css();
143
144 update_option(
145 $this->get_option_name(),
146 array(
147 'hash' => $hash,
148 'files' => $this->generate_css( $breakpoints ),
149 )
150 );
151 }
152
153 /**
154 * Write the built CSS files with the breakpoint values replaced into uploads.
155 *
156 * @param array $breakpoints - Breakpoints.
157 * @return array Paths of the written files, relative to the plugin directory.
158 */
159 protected function generate_css( $breakpoints ) {
160 $plugin_path = wp_normalize_path( $this->get_plugin_path() );
161 $build_dir = $plugin_path . 'build';
162 $output_dir = $this->get_output_dir();
163 $files = array();
164
165 if ( ! is_dir( $build_dir ) ) {
166 return $files;
167 }
168
169 try {
170 // CATCH_GET_CHILD skips a subdirectory PHP cannot open instead of ending the walk.
171 $iterator = new RecursiveIteratorIterator(
172 new RecursiveDirectoryIterator( $build_dir, FilesystemIterator::SKIP_DOTS ),
173 RecursiveIteratorIterator::LEAVES_ONLY,
174 RecursiveIteratorIterator::CATCH_GET_CHILD
175 );
176
177 foreach ( $iterator as $file ) {
178 if ( 'css' !== $file->getExtension() ) {
179 continue;
180 }
181
182 $path = wp_normalize_path( $file->getPathname() );
183 // phpcs:ignore
184 $css = file_get_contents( $path );
185
186 if ( false === $css ) {
187 continue;
188 }
189
190 $replaced = self::replace_breakpoints( $css, $breakpoints );
191
192 if ( $replaced === $css ) {
193 continue;
194 }
195
196 $relative = substr( $path, strlen( $plugin_path ) );
197 $target = $output_dir . '/' . $relative;
198
199 if ( ! wp_mkdir_p( dirname( $target ) ) ) {
200 continue;
201 }
202
203 // phpcs:ignore
204 if ( false === file_put_contents( $target, $replaced ) ) {
205 continue;
206 }
207
208 $files[] = $relative;
209 }
210 } catch ( UnexpectedValueException $e ) {
211 // `build` itself could not be opened; nothing was written.
212 unset( $e );
213 }
214
215 // WordPress derives the `-rtl.css` URL from the served one, so a file whose RTL twin
216 // exists in the build but was not written would give RTL sites a 404. Serve neither.
217 foreach ( $files as $index => $relative ) {
218 $rtl_twin = preg_replace( '/\.css$/', '-rtl.css', $relative );
219
220 if (
221 $rtl_twin !== $relative &&
222 ! in_array( $rtl_twin, $files, true ) &&
223 file_exists( $plugin_path . $rtl_twin )
224 ) {
225 wp_delete_file( $output_dir . '/' . $relative );
226 unset( $files[ $index ] );
227 }
228 }
229
230 $files = array_values( $files );
231 sort( $files );
232
233 return $files;
234 }
235
236 /**
237 * Remove the generated CSS and the option that lists it.
238 *
239 * @return void
240 */
241 protected function remove_generated_css() {
242 self::remove_dir( $this->get_output_dir() . '/build' );
243 delete_option( $this->get_option_name() );
244 }
245
246 /**
247 * Delete a directory with everything inside it.
248 *
249 * @param string $dir - Absolute path.
250 * @return void
251 */
252 public static function remove_dir( $dir ) {
253 if ( ! is_dir( $dir ) ) {
254 return;
255 }
256
257 try {
258 $iterator = new RecursiveIteratorIterator(
259 new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ),
260 RecursiveIteratorIterator::CHILD_FIRST
261 );
262
263 foreach ( $iterator as $item ) {
264 if ( $item->isDir() ) {
265 // phpcs:ignore
266 rmdir( $item->getPathname() );
267 } else {
268 wp_delete_file( $item->getPathname() );
269 }
270 }
271 } catch ( UnexpectedValueException $e ) {
272 // A directory PHP cannot open stays in place, with the directories above it.
273 return;
274 }
275
276 // phpcs:ignore
277 rmdir( $dir );
278 }
279
280 /**
281 * Replace the default breakpoint values inside `@media` preludes with the given ones.
282 *
283 * Only `(min-width: Npx)` and `(max-width: Npx)` whose N is a default breakpoint change,
284 * in one pass, so a custom value equal to another default is never replaced twice.
285 *
286 * @param string $css - CSS to process.
287 * @param array $breakpoints - Breakpoints keyed xs, sm, md, lg.
288 * @return string
289 */
290 public static function replace_breakpoints( $css, $breakpoints ) {
291 $defaults = array(
292 'xs' => self::$default_xs,
293 'sm' => self::$default_sm,
294 'md' => self::$default_md,
295 'lg' => self::$default_lg,
296 );
297 $map = array();
298
299 foreach ( $defaults as $name => $default ) {
300 if ( isset( $breakpoints[ $name ] ) && is_numeric( $breakpoints[ $name ] ) ) {
301 // Themes pass fractions such as 777.98 to keep a max-width query from
302 // overlapping the min-width one, so the value is kept as given.
303 $map[ $default ] = (string) ( 0 + $breakpoints[ $name ] );
304 }
305 }
306
307 return preg_replace_callback(
308 '/@media[^{;]*\{/',
309 function ( $prelude ) use ( $map ) {
310 return preg_replace_callback(
311 '/\(\s*(min|max)-width\s*:\s*(\d+)px\s*\)/',
312 function ( $query ) use ( $map ) {
313 $value = (int) $query[2];
314
315 if ( ! isset( $map[ $value ] ) ) {
316 return $query[0];
317 }
318
319 return '(' . $query[1] . '-width:' . $map[ $value ] . 'px)';
320 },
321 $prelude[0]
322 );
323 },
324 $css
325 );
326 }
327
328 /**
329 * Serve the generated CSS instead of the plugin file when custom breakpoints are set.
330 *
331 * @param string $src - Url to style.
332 * @return string
333 */
334 public function change_style_src_to_compile( $src ) {
335 $stored = $this->get_generated_css();
336
337 if ( ! $stored || empty( $stored['files'] ) ) {
338 return $src;
339 }
340
341 $plugin_url = $this->get_plugin_url();
342
343 if ( 0 !== strpos( $src, $plugin_url ) ) {
344 return $src;
345 }
346
347 $path = strtok( substr( $src, strlen( $plugin_url ) ), '?' );
348
349 if ( ! in_array( $path, $stored['files'], true ) ) {
350 return $src;
351 }
352
353 if ( ! file_exists( $this->get_output_dir() . '/' . $path ) ) {
354 return $src;
355 }
356
357 $upload_dir = wp_get_upload_dir();
358
359 return add_query_arg( 'ver', $stored['hash'], $upload_dir['baseurl'] . '/' . $this->plugin_name . '/' . $path );
360 }
361
362 /**
363 * Get Breakpoints.
364 */
365 public static function get_breakpoints() {
366 $xs = self::get_breakpoint_xs();
367 $xs = ( ! empty( $xs ) && $xs ) ? $xs : self::$default_xs;
368
369 $sm = self::get_breakpoint_sm();
370 $sm = ( ! empty( $sm ) && $sm ) ? $sm : self::$default_sm;
371
372 $md = self::get_breakpoint_md();
373 $md = ( ! empty( $md ) && $md ) ? $md : self::$default_md;
374
375 $lg = self::get_breakpoint_lg();
376 $lg = ( ! empty( $lg ) && $lg ) ? $lg : self::$default_lg;
377
378 return array(
379 'xs' => $xs,
380 'sm' => $sm,
381 'md' => $md,
382 'lg' => $lg,
383 );
384 }
385
386 /**
387 * Get default breakpoints.
388 *
389 * @return array
390 */
391 public static function get_default_breakpoints() {
392 return array(
393 'xs' => self::get_default_breakpoint_xs(),
394 'sm' => self::get_default_breakpoint_sm(),
395 'md' => self::get_default_breakpoint_md(),
396 'lg' => self::get_default_breakpoint_lg(),
397 );
398 }
399
400 /**
401 * Get breakpoints Hash
402 *
403 * @param array $breakpoints - Breakpoints.
404 * @return string
405 */
406 protected function get_breakpoints_hash( $breakpoints ) {
407 return md5(
408 wp_json_encode(
409 array_merge(
410 $breakpoints,
411 array(
412 $this->plugin_version,
413 )
414 )
415 )
416 );
417 }
418
419 /**
420 * Get default breakpoints Hash.
421 *
422 * @return string
423 */
424 protected function get_default_breakpoints_hash() {
425 return $this->get_breakpoints_hash(
426 array(
427 'xs' => self::$default_xs,
428 'sm' => self::$default_sm,
429 'md' => self::$default_md,
430 'lg' => self::$default_lg,
431 )
432 );
433 }
434
435 /**
436 * Get Default Extra Small Breakpoint.
437 *
438 * @return int
439 */
440 public static function get_default_breakpoint_xs() {
441 return apply_filters( 'gkt_default_breakpoint_xs', self::$default_xs );
442 }
443
444 /**
445 * Get Extra Small Breakpoint.
446 *
447 * @return int
448 */
449 public static function get_breakpoint_xs() {
450 return apply_filters( 'gkt_breakpoint_xs', self::get_default_breakpoint_xs() );
451 }
452
453 /**
454 * Get Default Mobile Breakpoint.
455 *
456 * @return int
457 */
458 public static function get_default_breakpoint_sm() {
459 return apply_filters( 'gkt_default_breakpoint_sm', self::$default_sm );
460 }
461
462 /**
463 * Get Mobile Breakpoint.
464 *
465 * @return int
466 */
467 public static function get_breakpoint_sm() {
468 return apply_filters( 'gkt_breakpoint_sm', self::get_default_breakpoint_sm() );
469 }
470
471 /**
472 * Get Default Tablet Breakpoint.
473 *
474 * @return int
475 */
476 public static function get_default_breakpoint_md() {
477 return apply_filters( 'gkt_default_breakpoint_md', self::$default_md );
478 }
479
480 /**
481 * Get Tablet Breakpoint.
482 *
483 * @return int
484 */
485 public static function get_breakpoint_md() {
486 return apply_filters( 'gkt_breakpoint_md', self::get_default_breakpoint_md() );
487 }
488
489 /**
490 * Get Default Desktop Breakpoint.
491 *
492 * @return int
493 */
494 public static function get_default_breakpoint_lg() {
495 return apply_filters( 'gkt_default_breakpoint_lg', self::$default_lg );
496 }
497
498 /**
499 * Get Desktop Breakpoint.
500 *
501 * @return int
502 */
503 public static function get_breakpoint_lg() {
504 return apply_filters( 'gkt_breakpoint_lg', self::get_default_breakpoint_lg() );
505 }
506 }
507 new GhostKit_Breakpoints();
508 }
509