@contentful/experiences-visual-editor-react 3.1.1 → 3.2.0-dev-20250812T1437-66fa8cb.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -631,35 +631,69 @@ const breakpointsRefinement$1 = (value, ctx) => {
631
631
  code: z.ZodIssueCode.custom,
632
632
  message: `The first breakpoint should include the following attributes: { "query": "*" }`,
633
633
  });
634
+ return;
634
635
  }
635
- const hasDuplicateIds = value.some((currentBreakpoint, currentBreakpointIndex) => {
636
- // check if the current breakpoint id is found in the rest of the array
637
- const breakpointIndex = value.findIndex((breakpoint) => breakpoint.id === currentBreakpoint.id);
638
- return breakpointIndex !== currentBreakpointIndex;
639
- });
636
+ // Return early if there's only one generic breakpoint
637
+ const hasNoBreakpointsStrategy = value.length === 1;
638
+ if (hasNoBreakpointsStrategy) {
639
+ return;
640
+ }
641
+ // Check if any breakpoint id occurs twice
642
+ const ids = value.map((breakpoint) => breakpoint.id);
643
+ const hasDuplicateIds = new Set(ids).size !== ids.length;
640
644
  if (hasDuplicateIds) {
641
645
  ctx.addIssue({
642
646
  code: z.ZodIssueCode.custom,
643
647
  message: `Breakpoint IDs must be unique`,
644
648
  });
649
+ return;
645
650
  }
646
- // Extract the queries boundary by removing the special characters around it
647
- const queries = value.map((bp) => bp.query === '*' ? bp.query : parseInt(bp.query.replace(/px|<|>/, '')));
648
- // sort updates queries array in place so we need to create a copy
649
- const originalQueries = [...queries];
650
- queries.sort((q1, q2) => {
651
- if (q1 === '*') {
652
- return -1;
651
+ // Skip the first one which is guaranteed to be a wildcard query
652
+ const nonBaseBreakpoints = value.slice(1);
653
+ const isMobileFirstStrategy = nonBaseBreakpoints[0].query.startsWith('>');
654
+ const isDesktopFirstStrategy = nonBaseBreakpoints[0].query.startsWith('<');
655
+ if (isMobileFirstStrategy) {
656
+ const areOperatorsEqual = nonBaseBreakpoints.every(({ query }) => query.startsWith('>'));
657
+ if (!areOperatorsEqual) {
658
+ ctx.addIssue({
659
+ code: z.ZodIssueCode.custom,
660
+ message: `Breakpoint queries must be in the format ">[size]px" for mobile-first strategy`,
661
+ });
653
662
  }
654
- if (q2 === '*') {
655
- return 1;
663
+ // Extract the queries boundary by removing the special characters around it
664
+ const queries = nonBaseBreakpoints.map((bp) => parseInt(bp.query.replace(/px|<|>/, '')));
665
+ // Starting with the third breakpoint, check that every query is higher than the one above
666
+ const isIncreasing = queries.every((value, index, array) => index === 0 || value > array[index - 1]);
667
+ if (!isIncreasing) {
668
+ ctx.addIssue({
669
+ code: z.ZodIssueCode.custom,
670
+ message: `When using a mobile-first strategy, all breakpoints must have strictly increasing pixel values`,
671
+ });
656
672
  }
657
- return q1 > q2 ? -1 : 1;
658
- });
659
- if (originalQueries.join('') !== queries.join('')) {
673
+ }
674
+ else if (isDesktopFirstStrategy) {
675
+ const areOperatorsEqual = nonBaseBreakpoints.every(({ query }) => query.startsWith('<'));
676
+ if (!areOperatorsEqual) {
677
+ ctx.addIssue({
678
+ code: z.ZodIssueCode.custom,
679
+ message: `Breakpoint queries must be in the format "<[size]px" for desktop-first strategy`,
680
+ });
681
+ }
682
+ // Extract the queries boundary by removing the special characters around it
683
+ const queries = nonBaseBreakpoints.map((bp) => parseInt(bp.query.replace(/px|<|>/, '')));
684
+ // Starting with the third breakpoint, check that every query is lower than the one above
685
+ const isDecreasing = queries.every((value, index, array) => index === 0 || value < array[index - 1]);
686
+ if (!isDecreasing) {
687
+ ctx.addIssue({
688
+ code: z.ZodIssueCode.custom,
689
+ message: `When using a desktop-first strategy, all breakpoints must have strictly decreasing pixel values`,
690
+ });
691
+ }
692
+ }
693
+ else if (!isMobileFirstStrategy && !isDesktopFirstStrategy) {
660
694
  ctx.addIssue({
661
695
  code: z.ZodIssueCode.custom,
662
- message: `Breakpoints should be ordered from largest to smallest pixel value`,
696
+ message: `You may only use a mobile-first or desktop-first strategy for breakpoints using '<' or '>' queries`,
663
697
  });
664
698
  }
665
699
  };
@@ -718,7 +752,7 @@ const ParametersSchema$1 = z.record(propertyKeySchema$1, ParameterSchema$1);
718
752
  const BreakpointSchema$1 = z
719
753
  .object({
720
754
  id: propertyKeySchema$1,
721
- query: z.string().regex(/^\*$|^<[0-9*]+px$/),
755
+ query: z.string().regex(/^\*$|^[<>][0-9*]+px$/),
722
756
  previewSize: z.string(),
723
757
  displayName: z.string(),
724
758
  displayIcon: z.enum(['desktop', 'tablet', 'mobile']).optional(),
@@ -996,7 +1030,7 @@ const toCSSMediaQuery = ({ query }) => {
996
1030
  };
997
1031
  // Remove this helper when upgrading to TypeScript 5.0 - https://github.com/microsoft/TypeScript/issues/48829
998
1032
  const findLast = (array, predicate) => {
999
- return array.reverse().find(predicate);
1033
+ return [...array].reverse().find(predicate);
1000
1034
  };
1001
1035
  // Initialise media query matchers. This won't include the always matching fallback breakpoint.
1002
1036
  const mediaQueryMatcher = (breakpoints) => {
@@ -1016,19 +1050,19 @@ const mediaQueryMatcher = (breakpoints) => {
1016
1050
  return [mediaQueryMatchers, mediaQueryMatches];
1017
1051
  };
1018
1052
  const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
1019
- // The breakpoints are ordered (desktop-first: descending by screen width)
1053
+ // The breakpoints are ordered (desktop-first: descending by screen width, mobile-first: ascending by screen width).
1020
1054
  const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
1021
1055
  id,
1022
1056
  index,
1023
1057
  // The fallback breakpoint with wildcard query will always match
1024
1058
  isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
1025
1059
  }));
1026
- // Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
1060
+ // Find the last breakpoint in the list that matches (desktop-first: the narrowest one, mobile-first: the widest one)
1027
1061
  const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
1028
1062
  return mostSpecificIndex ?? fallbackBreakpointIndex;
1029
1063
  };
1030
1064
  const getFallbackBreakpointIndex = (breakpoints) => {
1031
- // We assume that there will be a single breakpoint which uses the wildcard query.
1065
+ // The validation ensures that there will be exactly one breakpoint using the wildcard query.
1032
1066
  // If there is none, we just take the first one in the list.
1033
1067
  return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
1034
1068
  };
@@ -3314,35 +3348,69 @@ const breakpointsRefinement = (value, ctx) => {
3314
3348
  code: z.ZodIssueCode.custom,
3315
3349
  message: `The first breakpoint should include the following attributes: { "query": "*" }`,
3316
3350
  });
3351
+ return;
3317
3352
  }
3318
- const hasDuplicateIds = value.some((currentBreakpoint, currentBreakpointIndex) => {
3319
- // check if the current breakpoint id is found in the rest of the array
3320
- const breakpointIndex = value.findIndex((breakpoint) => breakpoint.id === currentBreakpoint.id);
3321
- return breakpointIndex !== currentBreakpointIndex;
3322
- });
3353
+ // Return early if there's only one generic breakpoint
3354
+ const hasNoBreakpointsStrategy = value.length === 1;
3355
+ if (hasNoBreakpointsStrategy) {
3356
+ return;
3357
+ }
3358
+ // Check if any breakpoint id occurs twice
3359
+ const ids = value.map((breakpoint) => breakpoint.id);
3360
+ const hasDuplicateIds = new Set(ids).size !== ids.length;
3323
3361
  if (hasDuplicateIds) {
3324
3362
  ctx.addIssue({
3325
3363
  code: z.ZodIssueCode.custom,
3326
3364
  message: `Breakpoint IDs must be unique`,
3327
3365
  });
3366
+ return;
3328
3367
  }
3329
- // Extract the queries boundary by removing the special characters around it
3330
- const queries = value.map((bp) => bp.query === '*' ? bp.query : parseInt(bp.query.replace(/px|<|>/, '')));
3331
- // sort updates queries array in place so we need to create a copy
3332
- const originalQueries = [...queries];
3333
- queries.sort((q1, q2) => {
3334
- if (q1 === '*') {
3335
- return -1;
3368
+ // Skip the first one which is guaranteed to be a wildcard query
3369
+ const nonBaseBreakpoints = value.slice(1);
3370
+ const isMobileFirstStrategy = nonBaseBreakpoints[0].query.startsWith('>');
3371
+ const isDesktopFirstStrategy = nonBaseBreakpoints[0].query.startsWith('<');
3372
+ if (isMobileFirstStrategy) {
3373
+ const areOperatorsEqual = nonBaseBreakpoints.every(({ query }) => query.startsWith('>'));
3374
+ if (!areOperatorsEqual) {
3375
+ ctx.addIssue({
3376
+ code: z.ZodIssueCode.custom,
3377
+ message: `Breakpoint queries must be in the format ">[size]px" for mobile-first strategy`,
3378
+ });
3336
3379
  }
3337
- if (q2 === '*') {
3338
- return 1;
3380
+ // Extract the queries boundary by removing the special characters around it
3381
+ const queries = nonBaseBreakpoints.map((bp) => parseInt(bp.query.replace(/px|<|>/, '')));
3382
+ // Starting with the third breakpoint, check that every query is higher than the one above
3383
+ const isIncreasing = queries.every((value, index, array) => index === 0 || value > array[index - 1]);
3384
+ if (!isIncreasing) {
3385
+ ctx.addIssue({
3386
+ code: z.ZodIssueCode.custom,
3387
+ message: `When using a mobile-first strategy, all breakpoints must have strictly increasing pixel values`,
3388
+ });
3339
3389
  }
3340
- return q1 > q2 ? -1 : 1;
3341
- });
3342
- if (originalQueries.join('') !== queries.join('')) {
3390
+ }
3391
+ else if (isDesktopFirstStrategy) {
3392
+ const areOperatorsEqual = nonBaseBreakpoints.every(({ query }) => query.startsWith('<'));
3393
+ if (!areOperatorsEqual) {
3394
+ ctx.addIssue({
3395
+ code: z.ZodIssueCode.custom,
3396
+ message: `Breakpoint queries must be in the format "<[size]px" for desktop-first strategy`,
3397
+ });
3398
+ }
3399
+ // Extract the queries boundary by removing the special characters around it
3400
+ const queries = nonBaseBreakpoints.map((bp) => parseInt(bp.query.replace(/px|<|>/, '')));
3401
+ // Starting with the third breakpoint, check that every query is lower than the one above
3402
+ const isDecreasing = queries.every((value, index, array) => index === 0 || value < array[index - 1]);
3403
+ if (!isDecreasing) {
3404
+ ctx.addIssue({
3405
+ code: z.ZodIssueCode.custom,
3406
+ message: `When using a desktop-first strategy, all breakpoints must have strictly decreasing pixel values`,
3407
+ });
3408
+ }
3409
+ }
3410
+ else if (!isMobileFirstStrategy && !isDesktopFirstStrategy) {
3343
3411
  ctx.addIssue({
3344
3412
  code: z.ZodIssueCode.custom,
3345
- message: `Breakpoints should be ordered from largest to smallest pixel value`,
3413
+ message: `You may only use a mobile-first or desktop-first strategy for breakpoints using '<' or '>' queries`,
3346
3414
  });
3347
3415
  }
3348
3416
  };
@@ -3401,7 +3469,7 @@ const ParametersSchema = z.record(propertyKeySchema, ParameterSchema);
3401
3469
  const BreakpointSchema = z
3402
3470
  .object({
3403
3471
  id: propertyKeySchema,
3404
- query: z.string().regex(/^\*$|^<[0-9*]+px$/),
3472
+ query: z.string().regex(/^\*$|^[<>][0-9*]+px$/),
3405
3473
  previewSize: z.string(),
3406
3474
  displayName: z.string(),
3407
3475
  displayIcon: z.enum(['desktop', 'tablet', 'mobile']).optional(),
@@ -4852,7 +4920,7 @@ const collectNodeCoordinates = (node, nodeToCoordinatesMap) => {
4852
4920
  node.children.forEach((child) => collectNodeCoordinates(child, nodeToCoordinatesMap));
4853
4921
  };
4854
4922
  function waitForImageToBeLoaded(imageNode) {
4855
- if (imageNode.complete) {
4923
+ if (imageNode.complete && (imageNode.naturalWidth > 0 || imageNode.naturalHeight > 0)) {
4856
4924
  return Promise.resolve();
4857
4925
  }
4858
4926
  return new Promise((resolve, reject) => {
@@ -4886,8 +4954,8 @@ const useCanvasGeometryUpdates = ({ tree, canvasMode }) => {
4886
4954
  trailing: true,
4887
4955
  }), []);
4888
4956
  const debouncedCollectImages = useMemo(() => debounce(() => {
4889
- return Array.from(document.querySelectorAll('img'));
4890
- }, 300, { leading: true, trailing: true }), []);
4957
+ setImages((prev) => ({ ...prev, allImages: findAllImages() }));
4958
+ }, 300, { trailing: true }), []);
4891
4959
  // Store tree in a ref to avoid the need to deactivate & reactivate the mutation observer
4892
4960
  // when the tree changes. This is important to avoid missing out on some mutation events.
4893
4961
  const treeRef = useRef(tree);
@@ -4896,13 +4964,17 @@ const useCanvasGeometryUpdates = ({ tree, canvasMode }) => {
4896
4964
  }, [tree]);
4897
4965
  // Handling window resize events
4898
4966
  useEffect(() => {
4899
- const resizeEventListener = () => debouncedUpdateGeometry(treeRef.current, 'resize');
4967
+ const resizeEventListener = () => {
4968
+ debouncedUpdateGeometry(treeRef.current, 'resize');
4969
+ // find all images on resize
4970
+ debouncedCollectImages();
4971
+ };
4900
4972
  window.addEventListener('resize', resizeEventListener);
4901
4973
  return () => window.removeEventListener('resize', resizeEventListener);
4902
- }, [debouncedUpdateGeometry]);
4974
+ }, [debouncedCollectImages, debouncedUpdateGeometry]);
4903
4975
  const [{ allImages, loadedImages }, setImages] = useState(() => {
4904
- const allImages = debouncedCollectImages();
4905
- const loadedImages = new WeakSet();
4976
+ const allImages = findAllImages();
4977
+ const loadedImages = new WeakMap();
4906
4978
  return { allImages, loadedImages };
4907
4979
  });
4908
4980
  // Handling DOM mutations
@@ -4910,8 +4982,7 @@ const useCanvasGeometryUpdates = ({ tree, canvasMode }) => {
4910
4982
  const observer = new MutationObserver(() => {
4911
4983
  debouncedUpdateGeometry(treeRef.current, 'mutation');
4912
4984
  // find all images on any DOM change
4913
- const allImages = debouncedCollectImages();
4914
- setImages((prevState) => ({ ...prevState, allImages }));
4985
+ debouncedCollectImages();
4915
4986
  });
4916
4987
  // send initial geometry in case the tree is empty
4917
4988
  debouncedUpdateGeometry(treeRef.current, 'mutation');
@@ -4927,13 +4998,14 @@ const useCanvasGeometryUpdates = ({ tree, canvasMode }) => {
4927
4998
  useEffect(() => {
4928
4999
  let isCurrent = true;
4929
5000
  allImages.forEach(async (imageNode) => {
4930
- if (loadedImages.has(imageNode)) {
5001
+ const lastSrc = loadedImages.get(imageNode);
5002
+ if (lastSrc === imageNode.currentSrc) {
4931
5003
  return;
4932
5004
  }
4933
5005
  // update the geometry after each image is loaded, as it can shift the layout
4934
5006
  await waitForImageToBeLoaded(imageNode);
4935
5007
  if (isCurrent) {
4936
- loadedImages.add(imageNode);
5008
+ loadedImages.set(imageNode, imageNode.currentSrc);
4937
5009
  debouncedUpdateGeometry(treeRef.current, 'imageLoad');
4938
5010
  }
4939
5011
  });
@@ -4960,6 +5032,9 @@ const useCanvasGeometryUpdates = ({ tree, canvasMode }) => {
4960
5032
  return () => document.removeEventListener('wheel', onWheel);
4961
5033
  }, [canvasMode]);
4962
5034
  };
5035
+ function findAllImages() {
5036
+ return Array.from(document.querySelectorAll('img'));
5037
+ }
4963
5038
 
4964
5039
  const RootRenderer = ({ inMemoryEntitiesStore, canvasMode }) => {
4965
5040
  useEditorSubscriber(inMemoryEntitiesStore);