PluginProbe
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts / trunk
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts vtrunk
2.8.14 2.8.13 2.8.12 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.10 2.8.11 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 trunk 1.0.1 1.0.2 1.1.0 1.1.1 All 147 releases
content-blocks-builder / src / features / carousel / utils.js

utils.js in Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts trunk, at src/features/carousel/utils.js

543 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * External dependencies
3 */
4 import { isObject, isString } from "lodash";
5
6 /**
7 * WordPress dependencies
8 */
9 import { __ } from "@wordpress/i18n";
10
11 /**
12 * Internal dependencies
13 */
14 import { isValidSettingObject } from "sdk/utils";
15 import { toType } from "../../utils/dom";
16
17 /**
18 * Breakpoint values in px
19 */
20 export const breakpointValues = {
21 sm: 576,
22 md: 768,
23 lg: 1024,
24 };
25
26 /**
27 * Define the list of effect options
28 */
29 export const effectOptions = [
30 { value: "slide", label: __("Slide", "content-blocks-builder") },
31 { value: "fade", label: __("Fade", "content-blocks-builder") },
32 { value: "flip", label: __("Flip", "content-blocks-builder") },
33 { value: "cube", label: __("Cube", "content-blocks-builder") },
34 { value: "cards", label: __("Cards", "content-blocks-builder") },
35 ];
36
37 /**
38 * If the current effect support multiple slides
39 *
40 * @param {String} effect
41 * @returns {Boolean}
42 */
43 export const isSingleSlideEffect = (effect) =>
44 ["fade", "flip", "cube", "cards", "creative"].includes(effect);
45
46 /**
47 * Default pagination
48 */
49 export const paginationDefault = {
50 enable: true,
51 bulletWidth: "12px",
52 bulletHeight: "12px",
53 spacing: "10px",
54 color: "",
55 inactiveColor: "",
56 opacity: 0.3,
57 dynamicBullets: true,
58 horizontalGap: "5px",
59 };
60
61 /**
62 * Default navigation
63 */
64 export const navigationDefault = {
65 enable: false,
66 size: "44px",
67 spacing: "10px",
68 color: "",
69 };
70
71 /**
72 * Default scrollbar
73 */
74 export const scrollbarDefault = {
75 enable: false,
76 trackSize: "5px",
77 draggable: true,
78 hide: false,
79 snapOnRelease: true,
80 trackColor: "",
81 sliderColor: "",
82 };
83
84 /**
85 * Default carousel settings
86 */
87 export const carouselSettingsDefault = {
88 speed: 500,
89 effect: "slide",
90 effectSettings: {},
91 loopType: "infinite", // rewind, none
92 autoplay: { enable: true, delay: 3000 },
93 slidesPerView: 1,
94 spaceBetween: 0,
95 enableResponsive: false,
96 breakpoints: {
97 lg: {
98 value: {
99 slidesPerView: 4, // 'auto'
100 spaceBetween: 0,
101 },
102 inherit: null,
103 },
104 md: { inherit: "lg" },
105 sm: { inherit: "lg" },
106 },
107 pagination: paginationDefault,
108 navigation: navigationDefault,
109 };
110
111 /**
112 * Build the carousel settings.
113 *
114 * @param {Object} carousel
115 * @param {Boolean} isDeprecatedAutoplay
116 */
117 export function buildCarouselSettings(
118 carousel,
119 { isDeprecatedAutoplay = false, isBlockEdit },
120 ) {
121 // Extract sub-values.
122 const {
123 speed,
124 loopType,
125 autoplay,
126 playPause = {},
127 direction = "horizontal",
128 effect: effectRaw,
129 effectSettings,
130 slidesPerView: slidesPerViewRaw,
131 slidesPerGroup,
132 spaceBetween,
133 centeredSlides,
134 centeredSlidesSettings,
135 enableResponsive,
136 breakpoints,
137 oneSlidePerViewInMobile = false,
138 pagination = paginationDefault,
139 navigation = navigationDefault,
140 scrollbar = scrollbarDefault,
141 equalHeight = false,
142 thumbs,
143 } = carousel ?? {};
144
145 let dataset = {};
146
147 // Speed
148 if (speed) {
149 dataset.speed = speed;
150 }
151
152 // Loop and rewind
153 dataset.loop = false;
154 dataset.rewind = false;
155 if (loopType === "infinite") {
156 dataset.loop = true;
157 } else if (loopType === "rewind") {
158 dataset.rewind = true;
159 }
160
161 // Autoplay
162 dataset.autoplay = false;
163 let enableAutoplay;
164 if (isDeprecatedAutoplay) {
165 enableAutoplay = autoplay && isObject(autoplay);
166 } else {
167 enableAutoplay = autoplay && isObject(autoplay) && autoplay?.enable;
168 }
169 if (enableAutoplay) {
170 if (isDeprecatedAutoplay) {
171 const { delay } = autoplay;
172 if (delay) {
173 dataset.autoplay = {
174 delay,
175 };
176 }
177 } else {
178 const { enable, ...restAutoplay } = autoplay;
179 dataset.autoplay = restAutoplay;
180 }
181
182 // Only in edit mode
183 if (isBlockEdit) {
184 if (!dataset.loop && !dataset.rewind) {
185 dataset.autoplay = { ...dataset.autoplay, stopOnLastSlide: true };
186 }
187 }
188 }
189
190 // Direction
191 if (direction) {
192 dataset.direction = direction;
193 }
194
195 // Effect
196 const effect =
197 toType(effectRaw) === "object" ? effectRaw?.name ?? "slide" : effectRaw;
198 if (effect) {
199 const { [`${effect}Effect`]: settings } = effectSettings ?? {};
200 let refinedEffect = effectRaw;
201 // Deprecated issue for very old version
202 if (
203 toType(effectRaw) === "object" &&
204 toType(effectRaw?.settings) === "array" &&
205 effectRaw.settings.length === 0
206 ) {
207 refinedEffect = { ...effectRaw, settings: {} };
208 }
209 dataset.effect = refinedEffect;
210
211 // Effect settings
212 if (effect === "fade") {
213 dataset.fadeEffect = { crossFade: true };
214 } else if (effect === "coverflow") {
215 if (isObject(settings)) {
216 dataset.coverflowEffect = settings;
217 } else {
218 dataset.coverflowEffect = {
219 slideShadows: false,
220 };
221 }
222 } else if (effect === "creative") {
223 if (isObject(settings)) {
224 dataset.creativeEffect = settings;
225 } else {
226 dataset.creativeEffect = {
227 prev: {
228 translate: ["-20%", 0, -1],
229 },
230 next: {
231 translate: ["100%", 0, 0],
232 },
233 };
234 }
235
236 dataset.direction = "horizontal";
237 }
238 }
239
240 // slidesPerView
241 if (isSingleSlideEffect(effect)) {
242 dataset.slidesPerView = 1;
243 } else {
244 let slidesPerView = slidesPerViewRaw;
245 if (toType(slidesPerViewRaw) === "object") {
246 slidesPerView = slidesPerViewRaw?.auto
247 ? "auto"
248 : slidesPerViewRaw?.value ?? 1;
249 }
250
251 if (slidesPerViewRaw) {
252 // Only in edit mode
253 if (isBlockEdit) {
254 dataset.slidesPerView = slidesPerView;
255 } else {
256 dataset.slidesPerView = slidesPerViewRaw;
257 }
258
259 if (slidesPerView > 1 && slidesPerGroup) {
260 dataset.slidesPerGroup = slidesPerGroup;
261 }
262 }
263
264 // Breakpoints
265 if (enableResponsive && isObject(breakpoints)) {
266 let breakpointAtts = {};
267 Object.keys(breakpoints).forEach((breakpoint) => {
268 const {
269 [breakpoint]: { value, inherit },
270 } = breakpoints;
271
272 let valueByBreakpoint;
273 if (value && isObject(value)) {
274 valueByBreakpoint = value;
275 }
276
277 if (!valueByBreakpoint && isString(inherit)) {
278 const { value: inheritValue } = breakpoints[inherit] ?? {};
279 if (inheritValue && isObject(inheritValue)) {
280 valueByBreakpoint = inheritValue;
281 }
282 }
283
284 if (isObject(valueByBreakpoint)) {
285 // Only in edit mode
286 if (isBlockEdit) {
287 valueByBreakpoint.slidesPerView = refineSlidesPerView(
288 valueByBreakpoint?.slidesPerView ?? 1,
289 );
290 }
291 breakpointAtts[breakpointValues[breakpoint]] = valueByBreakpoint;
292 }
293 });
294
295 dataset.breakpoints = breakpointAtts;
296
297 // Only in edit mode
298 if (isBlockEdit) {
299 const { slidesPerView, slidesPerGroup, spaceBetween } =
300 Object.values(breakpointAtts)[0] ?? {};
301 if (slidesPerView) {
302 dataset.slidesPerView = slidesPerView;
303 if (slidesPerView > 1 && slidesPerGroup) {
304 dataset.slidesPerGroup = slidesPerGroup;
305 }
306 if (spaceBetween) {
307 dataset.spaceBetween = spaceBetween;
308 }
309 }
310 }
311 } else {
312 if (oneSlidePerViewInMobile) {
313 let breakpointAtts = {
314 [breakpointValues.sm]: {
315 slidesPerView: 1,
316 spaceBetween: 0,
317 },
318 [breakpointValues.md]: {
319 // Only in edit mode
320 slidesPerView: isBlockEdit ? slidesPerView : slidesPerViewRaw,
321 spaceBetween,
322 },
323 };
324
325 dataset.breakpoints = breakpointAtts;
326 }
327 }
328 }
329
330 // centeredSlides
331 dataset.centeredSlides = centeredSlides;
332 if (
333 centeredSlides &&
334 effect === "slide" &&
335 centeredSlidesSettings &&
336 centeredSlidesSettings?.enable
337 ) {
338 dataset.centeredSlidesSettings = centeredSlidesSettings;
339 }
340
341 // Remove the centeredSlides
342 if (isBlockEdit) {
343 if (
344 isSingleSlideEffect(effect) ||
345 (dataset.slidesPerView <= 1 && !enableResponsive)
346 ) {
347 delete dataset.centeredSlides;
348 delete dataset.centeredSlidesSettings;
349 }
350 }
351
352 // spaceBetween
353 if (spaceBetween) {
354 dataset.spaceBetween = spaceBetween;
355 }
356
357 // Pagination
358 dataset.pagination = !!pagination?.enable;
359 dataset.paginationSettings = pagination;
360
361 // Navigation
362 dataset.navigation = !!navigation?.enable;
363 dataset.navigationSettings = navigation;
364
365 // Play/pause since 2.8.2
366 if (playPause?.enable) {
367 dataset.playPause = playPause;
368 }
369
370 // Scrollbar
371 dataset.scrollbar = !!scrollbar?.enable;
372 dataset.scrollbarSettings = scrollbar;
373
374 // Equal height?
375 if (equalHeight) {
376 // Only add the property when it is true to prevent from invalid content
377 dataset.equalHeight = equalHeight;
378 }
379
380 if (thumbs && thumbs?.enable) {
381 dataset.thumbsSettings = thumbs;
382 }
383
384 return dataset;
385 }
386
387 export const buildCarouselEditStyle = ({
388 selector,
389 wraperSelector = ".carousel__inner",
390 itemSelector = ".is-carousel-item",
391 cssVariables,
392 }) => {
393 let styles = `
394 ${selector} {${cssVariables}}
395 /* Appender */
396 ${selector}.cbb-carousel-edit > ${wraperSelector} > .block-list-appender .block-list-appender__toggle {
397 display: flex;
398 }
399 ${selector}.cbb-carousel-edit > ${wraperSelector} > .block-list-appender {
400 bottom: 17px;
401 left: calc(var(--bb--carousel--inner-width, 100%) - 24px);
402 }
403 /* Edit mode */
404 ${selector}.cbb-carousel-edit {
405 overflow-x: auto;
406 overflow-y: hidden;
407 }
408 ${selector}.cbb-carousel-edit > ${wraperSelector} {
409 position: relative;
410 display: flex;
411 gap: var(--bb--carousel--spacebetween, 0);
412 max-width: none;
413 scroll-snap-type: x mandatory;
414 }
415 ${selector}.cbb-carousel-edit > ${wraperSelector} > ${itemSelector} {
416 flex-shrink: 0;
417 scroll-snap-align: start;
418 }
419 ${selector}.cbb-carousel-edit > ${wraperSelector} > ${itemSelector} {
420 width: var(--bb--carousel--item-width, 100%) !important;
421 }
422 `;
423
424 return styles;
425 };
426
427 /**
428 * Build style for EDIT mode
429 *
430 * @param {Object} carousel
431 * @param {Integer} slideCount
432 * @param {String} devicePrefix
433 * @returns {String}
434 */
435 export const buildEditLayoutStyle = (carousel, slideCount, devicePrefix) => {
436 const {
437 effect,
438 slidesPerView,
439 spaceBetween,
440 enableResponsive,
441 breakpoints = {},
442 oneSlidePerViewInMobile = false,
443 } = carousel;
444
445 const styles = [];
446 let slidesPerViewValue = refineSlidesPerView(slidesPerView);
447 let spaceBetweenValue = spaceBetween;
448
449 if (isSingleSlideEffect(effect)) {
450 slidesPerViewValue = 1;
451 } else {
452 if (enableResponsive && isValidSettingObject(breakpoints)) {
453 let { value, inherit } = breakpoints[devicePrefix] ?? {};
454 if (!isValidSettingObject(value) && inherit) {
455 value = (breakpoints[inherit] ?? {})["value"] ?? {};
456 }
457
458 if (isValidSettingObject(value)) {
459 if (value?.slidesPerView) {
460 slidesPerViewValue = refineSlidesPerView(value.slidesPerView);
461 }
462 if (value?.spaceBetween) {
463 spaceBetweenValue = value.spaceBetween;
464 }
465 }
466 } else {
467 if (oneSlidePerViewInMobile && devicePrefix === "sm") {
468 slidesPerViewValue = 1;
469 }
470 }
471 }
472
473 if (slidesPerViewValue && slidesPerViewValue !== "auto") {
474 styles.push(`--bb--carousel--slidesperview: ${slidesPerViewValue};`);
475 slidesPerViewValue = parseFloat(slidesPerViewValue);
476 slidesPerViewValue = isNaN(slidesPerViewValue) ? 1 : slidesPerViewValue;
477 styles.push(
478 `--bb--carousel--inner-width: calc(${
479 (1 / slidesPerViewValue) * slideCount * 100
480 }%);`,
481 );
482
483 styles.push(
484 `--bb--carousel--item-width: calc(${100 / slidesPerViewValue}% - ${
485 ((slidesPerViewValue - 1) * spaceBetweenValue) / slidesPerViewValue
486 }px);`,
487 );
488 }
489
490 if (spaceBetweenValue) {
491 styles.push(`--bb--carousel--spacebetween: ${spaceBetweenValue}px;`);
492 }
493
494 return styles.join("");
495 };
496
497 /**
498 * Get the valid value for slidesPerView
499 *
500 * @param {mixed} rawValue
501 * @returns {String}
502 */
503 const refineSlidesPerView = (rawValue) => {
504 const value =
505 toType(rawValue) === "object"
506 ? rawValue?.auto
507 ? "auto"
508 : rawValue?.value
509 : rawValue;
510
511 if (value === "auto") return value;
512
513 const numericValue = parseFloat(value);
514 return isNaN(numericValue) ? 1 : numericValue;
515 };
516
517 export const minimumSlidesNeeded = (
518 spv,
519 spg = 1,
520 centeredSlides = false,
521 breakpoints = false,
522 ) => {
523 if (breakpoints) {
524 spv = breakpoints?.slidesPerView;
525 spg = breakpoints?.slidesPerGroup ?? 1;
526 }
527
528 if (!spv || spv === "auto") {
529 return 0;
530 }
531
532 spv = Math.ceil(parseFloat(spv));
533 if (centeredSlides && spv % 2 === 0) spv += 1;
534
535 let loopedSlides = centeredSlides ? Math.max(spg, Math.ceil(spv / 2)) : spg;
536
537 if (loopedSlides % spg !== 0) {
538 loopedSlides += spg - (loopedSlides % spg);
539 }
540
541 return spv + loopedSlides;
542 };
543