vue-datocms 3.0.2 → 4.0.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.cjs.js CHANGED
@@ -27,8 +27,7 @@ const useInView = ({ threshold, rootMargin }) => {
27
27
  if (isIntersectionObserverAvailable()) {
28
28
  observer.value = new IntersectionObserver(
29
29
  (entries) => {
30
- const image = entries[0];
31
- if (image.isIntersecting && observer.value) {
30
+ if (entries.some(({ isIntersecting }) => isIntersecting) && observer.value) {
32
31
  inView.value = true;
33
32
  observer.value.disconnect();
34
33
  }
@@ -191,6 +190,36 @@ const imageShowStrategy = ({ lazyLoad, loaded }) => {
191
190
  }
192
191
  return true;
193
192
  };
193
+ const buildSrcSet = (src, width, candidateMultipliers) => {
194
+ if (!src || !width) {
195
+ return void 0;
196
+ }
197
+ return candidateMultipliers.map((multiplier) => {
198
+ const url = new URL(src);
199
+ if (multiplier !== 1) {
200
+ url.searchParams.set("dpr", `${multiplier}`);
201
+ const maxH = url.searchParams.get("max-h");
202
+ const maxW = url.searchParams.get("max-w");
203
+ if (maxH) {
204
+ url.searchParams.set(
205
+ "max-h",
206
+ `${Math.floor(parseInt(maxH) * multiplier)}`
207
+ );
208
+ }
209
+ if (maxW) {
210
+ url.searchParams.set(
211
+ "max-w",
212
+ `${Math.floor(parseInt(maxW) * multiplier)}`
213
+ );
214
+ }
215
+ }
216
+ const finalWidth = Math.floor(width * multiplier);
217
+ if (finalWidth < 50) {
218
+ return null;
219
+ }
220
+ return `${url.toString()} ${finalWidth}w`;
221
+ }).filter(Boolean).join(",");
222
+ };
194
223
  const Image = vueDemi.defineComponent({
195
224
  name: "DatocmsImage",
196
225
  props: {
@@ -240,6 +269,24 @@ const Image = vueDemi.defineComponent({
240
269
  },
241
270
  objectPosition: {
242
271
  type: String
272
+ },
273
+ usePlaceholder: {
274
+ type: Boolean,
275
+ default: true
276
+ },
277
+ sizes: {
278
+ type: String
279
+ },
280
+ priority: {
281
+ type: Boolean,
282
+ default: false
283
+ },
284
+ srcSetCandidates: {
285
+ type: Array,
286
+ validator: (values) => values.every((value) => {
287
+ return typeof value === "number";
288
+ }),
289
+ default: () => [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4]
243
290
  }
244
291
  },
245
292
  setup(props) {
@@ -251,21 +298,33 @@ const Image = vueDemi.defineComponent({
251
298
  threshold: props.intersectionThreshold || props.intersectionTreshold || 0,
252
299
  rootMargin: props.intersectionMargin || "0px 0px 0px 0px"
253
300
  });
301
+ const computedLazyLoad = vueDemi.ref(props.priority ? false : props.lazyLoad);
302
+ const imageRef = vueDemi.ref();
303
+ vueDemi.watchEffect(() => {
304
+ if (!imageRef.value) {
305
+ return;
306
+ }
307
+ if (imageRef.value.complete && imageRef.value.naturalWidth) {
308
+ handleLoad();
309
+ }
310
+ });
254
311
  return {
255
312
  inView,
256
313
  elRef,
257
314
  loaded,
258
- handleLoad
315
+ handleLoad,
316
+ computedLazyLoad,
317
+ imageRef
259
318
  };
260
319
  },
261
320
  render() {
262
321
  const addImage = imageAddStrategy({
263
- lazyLoad: this.lazyLoad,
322
+ lazyLoad: this.computedLazyLoad,
264
323
  inView: this.inView,
265
324
  loaded: this.loaded
266
325
  });
267
326
  const showImage = imageShowStrategy({
268
- lazyLoad: this.lazyLoad,
327
+ lazyLoad: this.computedLazyLoad,
269
328
  inView: this.inView,
270
329
  loaded: this.loaded
271
330
  });
@@ -273,30 +332,30 @@ const Image = vueDemi.defineComponent({
273
332
  ...vueDemi.isVue2 && {
274
333
  props: {
275
334
  srcset: this.data.webpSrcSet,
276
- sizes: this.data.sizes,
335
+ sizes: this.sizes ?? this.data.sizes ?? void 0,
277
336
  type: "image/webp"
278
337
  }
279
338
  },
280
339
  ...vueDemi.isVue3 && {
281
340
  srcset: this.data.webpSrcSet,
282
- sizes: this.data.sizes,
341
+ sizes: this.sizes ?? this.data.sizes ?? void 0,
283
342
  type: "image/webp"
284
343
  }
285
344
  });
286
345
  const regularSource = this.data.srcSet && vueDemi.h(Source, {
287
346
  ...vueDemi.isVue2 && {
288
347
  props: {
289
- srcset: this.data.srcSet,
290
- sizes: this.data.sizes
348
+ srcset: this.data.srcSet ?? buildSrcSet(this.data.src, this.data.width, this.srcSetCandidates),
349
+ sizes: this.sizes ?? this.data.sizes ?? void 0
291
350
  }
292
351
  },
293
352
  ...vueDemi.isVue3 && {
294
- srcset: this.data.srcSet,
295
- sizes: this.data.sizes
353
+ srcset: this.data.srcSet ?? buildSrcSet(this.data.src, this.data.width, this.srcSetCandidates),
354
+ sizes: this.sizes ?? this.data.sizes ?? void 0
296
355
  }
297
356
  });
298
357
  const transition = typeof this.fadeInDuration === "undefined" || this.fadeInDuration > 0 ? `opacity ${this.fadeInDuration || 500}ms ${this.fadeInDuration || 500}ms` : void 0;
299
- const placeholder = vueDemi.h("div", {
358
+ const placeholder = this.usePlaceholder && (this.data.bgColor || this.data.base64) ? vueDemi.h("div", {
300
359
  style: {
301
360
  backgroundImage: this.data.base64 ? `url(${this.data.base64})` : null,
302
361
  backgroundColor: this.data.bgColor,
@@ -305,11 +364,15 @@ const Image = vueDemi.defineComponent({
305
364
  objectFit: this.objectFit,
306
365
  objectPosition: this.objectPosition,
307
366
  transition,
308
- ...absolutePositioning
367
+ position: "absolute",
368
+ left: "-5%",
369
+ top: "-5%",
370
+ width: "110%",
371
+ height: "110%"
309
372
  }
310
- });
373
+ }) : null;
311
374
  const { width, aspectRatio } = this.data;
312
- const height = this.data.height || width / aspectRatio;
375
+ const height = this.data.height ?? (aspectRatio ? width / aspectRatio : 0);
313
376
  const sizer = this.layout !== "fill" ? vueDemi.h(Sizer, {
314
377
  ...vueDemi.isVue2 && {
315
378
  props: {
@@ -350,7 +413,8 @@ const Image = vueDemi.defineComponent({
350
413
  attrs: {
351
414
  src: this.data.src,
352
415
  alt: this.data.alt,
353
- title: this.data.title
416
+ title: this.data.title,
417
+ fetchpriority: this.priority ? "high" : void 0
354
418
  },
355
419
  on: {
356
420
  load: this.handleLoad
@@ -360,16 +424,18 @@ const Image = vueDemi.defineComponent({
360
424
  src: this.data.src,
361
425
  alt: this.data.alt,
362
426
  title: this.data.title,
427
+ fetchpriority: this.priority ? "high" : void 0,
363
428
  onLoad: this.handleLoad
364
429
  },
430
+ ref: "imageRef",
365
431
  class: this.pictureClass,
366
432
  style: {
367
- ...absolutePositioning,
368
- ...this.pictureStyle,
369
433
  opacity: showImage ? 1 : 0,
370
434
  transition,
435
+ ...absolutePositioning,
371
436
  objectFit: this.objectFit,
372
- objectPosition: this.objectPosition
437
+ objectPosition: this.objectPosition,
438
+ ...this.pictureStyle
373
439
  }
374
440
  })
375
441
  ]),
@@ -391,8 +457,14 @@ const Image = vueDemi.defineComponent({
391
457
  alt: this.data.alt,
392
458
  title: this.data.title,
393
459
  class: this.pictureClass,
394
- style: toCss({ ...this.pictureStyle, ...absolutePositioning }),
395
- loading: "lazy"
460
+ style: toCss({
461
+ ...absolutePositioning,
462
+ objectFit: this.objectFit,
463
+ objectPosition: this.objectPosition,
464
+ ...this.pictureStyle
465
+ }),
466
+ loading: this.computedLazyLoad ? "lazy" : void 0,
467
+ fetchpriority: this.priority ? "high" : void 0
396
468
  })
397
469
  ])
398
470
  }
@@ -414,7 +486,8 @@ const Image = vueDemi.defineComponent({
414
486
  title: this.data.title,
415
487
  class: this.pictureClass,
416
488
  style: toCss({ ...this.pictureStyle, ...absolutePositioning }),
417
- loading: "lazy"
489
+ loading: this.computedLazyLoad ? "lazy" : void 0,
490
+ fetchpriority: this.priority ? "high" : void 0
418
491
  })
419
492
  ])
420
493
  }
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ import { Options, ConnectionStatus } from 'datocms-listen';
8
8
 
9
9
  declare type ResponsiveImageType = {
10
10
  /** The aspect ratio (width/height) of the image */
11
- aspectRatio: number;
11
+ aspectRatio?: number;
12
12
  /** A base64-encoded thumbnail to offer during image loading */
13
13
  base64?: string;
14
14
  /** The height of the image */
@@ -100,11 +100,47 @@ declare const Image: vue_demi.DefineComponent<{
100
100
  objectPosition: {
101
101
  type: StringConstructor;
102
102
  };
103
+ /** Whether the component should use a blurred image placeholder */
104
+ usePlaceholder: {
105
+ type: BooleanConstructor;
106
+ default: boolean;
107
+ };
108
+ /**
109
+ * The HTML5 `sizes` attribute for the image
110
+ *
111
+ * Learn more about srcset and sizes:
112
+ * -> https://web.dev/learn/design/responsive-images/#sizes
113
+ * -> https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-sizes
114
+ **/
115
+ sizes: {
116
+ type: StringConstructor;
117
+ };
118
+ /**
119
+ * When true, the image will be considered high priority. Lazy loading is automatically disabled, and fetchpriority="high" is added to the image.
120
+ * You should use the priority property on any image detected as the Largest Contentful Paint (LCP) element. It may be appropriate to have multiple priority images, as different images may be the LCP element for different viewport sizes.
121
+ * Should only be used when the image is visible above the fold.
122
+ **/
123
+ priority: {
124
+ type: BooleanConstructor;
125
+ default: boolean;
126
+ };
127
+ /**
128
+ * If `data` does not contain `srcSet`, the candidates for the `srcset` of the image will be auto-generated based on these width multipliers
129
+ *
130
+ * Default candidate multipliers are [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4]
131
+ **/
132
+ srcSetCandidates: {
133
+ type: ArrayConstructor;
134
+ validator: (values: any[]) => values is number[];
135
+ default: () => number[];
136
+ };
103
137
  }, {
104
138
  inView: vue_demi.Ref<boolean>;
105
139
  elRef: vue_demi.Ref<HTMLElement | null>;
106
140
  loaded: vue_demi.Ref<boolean>;
107
141
  handleLoad: () => void;
142
+ computedLazyLoad: vue_demi.Ref<boolean>;
143
+ imageRef: vue_demi.Ref<HTMLImageElement | undefined>;
108
144
  }, unknown, {}, {}, vue_demi.ComponentOptionsMixin, vue_demi.ComponentOptionsMixin, {}, string, vue_demi.VNodeProps & vue_demi.AllowedComponentProps & vue_demi.ComponentCustomProps, Readonly<vue_demi.ExtractPropTypes<{
109
145
  /** The actual response you get from a DatoCMS `responsiveImage` GraphQL query */
110
146
  data: {
@@ -175,6 +211,40 @@ declare const Image: vue_demi.DefineComponent<{
175
211
  objectPosition: {
176
212
  type: StringConstructor;
177
213
  };
214
+ /** Whether the component should use a blurred image placeholder */
215
+ usePlaceholder: {
216
+ type: BooleanConstructor;
217
+ default: boolean;
218
+ };
219
+ /**
220
+ * The HTML5 `sizes` attribute for the image
221
+ *
222
+ * Learn more about srcset and sizes:
223
+ * -> https://web.dev/learn/design/responsive-images/#sizes
224
+ * -> https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-sizes
225
+ **/
226
+ sizes: {
227
+ type: StringConstructor;
228
+ };
229
+ /**
230
+ * When true, the image will be considered high priority. Lazy loading is automatically disabled, and fetchpriority="high" is added to the image.
231
+ * You should use the priority property on any image detected as the Largest Contentful Paint (LCP) element. It may be appropriate to have multiple priority images, as different images may be the LCP element for different viewport sizes.
232
+ * Should only be used when the image is visible above the fold.
233
+ **/
234
+ priority: {
235
+ type: BooleanConstructor;
236
+ default: boolean;
237
+ };
238
+ /**
239
+ * If `data` does not contain `srcSet`, the candidates for the `srcset` of the image will be auto-generated based on these width multipliers
240
+ *
241
+ * Default candidate multipliers are [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4]
242
+ **/
243
+ srcSetCandidates: {
244
+ type: ArrayConstructor;
245
+ validator: (values: any[]) => values is number[];
246
+ default: () => number[];
247
+ };
178
248
  }>>, {
179
249
  explicitWidth: boolean;
180
250
  lazyLoad: boolean;
@@ -183,6 +253,9 @@ declare const Image: vue_demi.DefineComponent<{
183
253
  rootStyle: Record<string, any>;
184
254
  pictureStyle: Record<string, any>;
185
255
  layout: string;
256
+ usePlaceholder: boolean;
257
+ priority: boolean;
258
+ srcSetCandidates: unknown[];
186
259
  }>;
187
260
  declare const DatocmsImagePlugin: {
188
261
  install: (Vue: any) => void;
@@ -21,8 +21,7 @@ const useInView = ({ threshold, rootMargin }) => {
21
21
  if (isIntersectionObserverAvailable()) {
22
22
  observer.value = new IntersectionObserver(
23
23
  (entries) => {
24
- const image = entries[0];
25
- if (image.isIntersecting && observer.value) {
24
+ if (entries.some(({ isIntersecting }) => isIntersecting) && observer.value) {
26
25
  inView.value = true;
27
26
  observer.value.disconnect();
28
27
  }
@@ -185,6 +184,36 @@ const imageShowStrategy = ({ lazyLoad, loaded }) => {
185
184
  }
186
185
  return true;
187
186
  };
187
+ const buildSrcSet = (src, width, candidateMultipliers) => {
188
+ if (!src || !width) {
189
+ return void 0;
190
+ }
191
+ return candidateMultipliers.map((multiplier) => {
192
+ const url = new URL(src);
193
+ if (multiplier !== 1) {
194
+ url.searchParams.set("dpr", `${multiplier}`);
195
+ const maxH = url.searchParams.get("max-h");
196
+ const maxW = url.searchParams.get("max-w");
197
+ if (maxH) {
198
+ url.searchParams.set(
199
+ "max-h",
200
+ `${Math.floor(parseInt(maxH) * multiplier)}`
201
+ );
202
+ }
203
+ if (maxW) {
204
+ url.searchParams.set(
205
+ "max-w",
206
+ `${Math.floor(parseInt(maxW) * multiplier)}`
207
+ );
208
+ }
209
+ }
210
+ const finalWidth = Math.floor(width * multiplier);
211
+ if (finalWidth < 50) {
212
+ return null;
213
+ }
214
+ return `${url.toString()} ${finalWidth}w`;
215
+ }).filter(Boolean).join(",");
216
+ };
188
217
  const Image = defineComponent({
189
218
  name: "DatocmsImage",
190
219
  props: {
@@ -234,6 +263,24 @@ const Image = defineComponent({
234
263
  },
235
264
  objectPosition: {
236
265
  type: String
266
+ },
267
+ usePlaceholder: {
268
+ type: Boolean,
269
+ default: true
270
+ },
271
+ sizes: {
272
+ type: String
273
+ },
274
+ priority: {
275
+ type: Boolean,
276
+ default: false
277
+ },
278
+ srcSetCandidates: {
279
+ type: Array,
280
+ validator: (values) => values.every((value) => {
281
+ return typeof value === "number";
282
+ }),
283
+ default: () => [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4]
237
284
  }
238
285
  },
239
286
  setup(props) {
@@ -245,21 +292,33 @@ const Image = defineComponent({
245
292
  threshold: props.intersectionThreshold || props.intersectionTreshold || 0,
246
293
  rootMargin: props.intersectionMargin || "0px 0px 0px 0px"
247
294
  });
295
+ const computedLazyLoad = ref(props.priority ? false : props.lazyLoad);
296
+ const imageRef = ref();
297
+ watchEffect(() => {
298
+ if (!imageRef.value) {
299
+ return;
300
+ }
301
+ if (imageRef.value.complete && imageRef.value.naturalWidth) {
302
+ handleLoad();
303
+ }
304
+ });
248
305
  return {
249
306
  inView,
250
307
  elRef,
251
308
  loaded,
252
- handleLoad
309
+ handleLoad,
310
+ computedLazyLoad,
311
+ imageRef
253
312
  };
254
313
  },
255
314
  render() {
256
315
  const addImage = imageAddStrategy({
257
- lazyLoad: this.lazyLoad,
316
+ lazyLoad: this.computedLazyLoad,
258
317
  inView: this.inView,
259
318
  loaded: this.loaded
260
319
  });
261
320
  const showImage = imageShowStrategy({
262
- lazyLoad: this.lazyLoad,
321
+ lazyLoad: this.computedLazyLoad,
263
322
  inView: this.inView,
264
323
  loaded: this.loaded
265
324
  });
@@ -267,30 +326,30 @@ const Image = defineComponent({
267
326
  ...isVue2 && {
268
327
  props: {
269
328
  srcset: this.data.webpSrcSet,
270
- sizes: this.data.sizes,
329
+ sizes: this.sizes ?? this.data.sizes ?? void 0,
271
330
  type: "image/webp"
272
331
  }
273
332
  },
274
333
  ...isVue3 && {
275
334
  srcset: this.data.webpSrcSet,
276
- sizes: this.data.sizes,
335
+ sizes: this.sizes ?? this.data.sizes ?? void 0,
277
336
  type: "image/webp"
278
337
  }
279
338
  });
280
339
  const regularSource = this.data.srcSet && h(Source, {
281
340
  ...isVue2 && {
282
341
  props: {
283
- srcset: this.data.srcSet,
284
- sizes: this.data.sizes
342
+ srcset: this.data.srcSet ?? buildSrcSet(this.data.src, this.data.width, this.srcSetCandidates),
343
+ sizes: this.sizes ?? this.data.sizes ?? void 0
285
344
  }
286
345
  },
287
346
  ...isVue3 && {
288
- srcset: this.data.srcSet,
289
- sizes: this.data.sizes
347
+ srcset: this.data.srcSet ?? buildSrcSet(this.data.src, this.data.width, this.srcSetCandidates),
348
+ sizes: this.sizes ?? this.data.sizes ?? void 0
290
349
  }
291
350
  });
292
351
  const transition = typeof this.fadeInDuration === "undefined" || this.fadeInDuration > 0 ? `opacity ${this.fadeInDuration || 500}ms ${this.fadeInDuration || 500}ms` : void 0;
293
- const placeholder = h("div", {
352
+ const placeholder = this.usePlaceholder && (this.data.bgColor || this.data.base64) ? h("div", {
294
353
  style: {
295
354
  backgroundImage: this.data.base64 ? `url(${this.data.base64})` : null,
296
355
  backgroundColor: this.data.bgColor,
@@ -299,11 +358,15 @@ const Image = defineComponent({
299
358
  objectFit: this.objectFit,
300
359
  objectPosition: this.objectPosition,
301
360
  transition,
302
- ...absolutePositioning
361
+ position: "absolute",
362
+ left: "-5%",
363
+ top: "-5%",
364
+ width: "110%",
365
+ height: "110%"
303
366
  }
304
- });
367
+ }) : null;
305
368
  const { width, aspectRatio } = this.data;
306
- const height = this.data.height || width / aspectRatio;
369
+ const height = this.data.height ?? (aspectRatio ? width / aspectRatio : 0);
307
370
  const sizer = this.layout !== "fill" ? h(Sizer, {
308
371
  ...isVue2 && {
309
372
  props: {
@@ -344,7 +407,8 @@ const Image = defineComponent({
344
407
  attrs: {
345
408
  src: this.data.src,
346
409
  alt: this.data.alt,
347
- title: this.data.title
410
+ title: this.data.title,
411
+ fetchpriority: this.priority ? "high" : void 0
348
412
  },
349
413
  on: {
350
414
  load: this.handleLoad
@@ -354,16 +418,18 @@ const Image = defineComponent({
354
418
  src: this.data.src,
355
419
  alt: this.data.alt,
356
420
  title: this.data.title,
421
+ fetchpriority: this.priority ? "high" : void 0,
357
422
  onLoad: this.handleLoad
358
423
  },
424
+ ref: "imageRef",
359
425
  class: this.pictureClass,
360
426
  style: {
361
- ...absolutePositioning,
362
- ...this.pictureStyle,
363
427
  opacity: showImage ? 1 : 0,
364
428
  transition,
429
+ ...absolutePositioning,
365
430
  objectFit: this.objectFit,
366
- objectPosition: this.objectPosition
431
+ objectPosition: this.objectPosition,
432
+ ...this.pictureStyle
367
433
  }
368
434
  })
369
435
  ]),
@@ -385,8 +451,14 @@ const Image = defineComponent({
385
451
  alt: this.data.alt,
386
452
  title: this.data.title,
387
453
  class: this.pictureClass,
388
- style: toCss({ ...this.pictureStyle, ...absolutePositioning }),
389
- loading: "lazy"
454
+ style: toCss({
455
+ ...absolutePositioning,
456
+ objectFit: this.objectFit,
457
+ objectPosition: this.objectPosition,
458
+ ...this.pictureStyle
459
+ }),
460
+ loading: this.computedLazyLoad ? "lazy" : void 0,
461
+ fetchpriority: this.priority ? "high" : void 0
390
462
  })
391
463
  ])
392
464
  }
@@ -408,7 +480,8 @@ const Image = defineComponent({
408
480
  title: this.data.title,
409
481
  class: this.pictureClass,
410
482
  style: toCss({ ...this.pictureStyle, ...absolutePositioning }),
411
- loading: "lazy"
483
+ loading: this.computedLazyLoad ? "lazy" : void 0,
484
+ fetchpriority: this.priority ? "high" : void 0
412
485
  })
413
486
  ])
414
487
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue-datocms",
3
- "version": "3.0.2",
3
+ "version": "4.0.0",
4
4
  "description": "A set of components and utilities to work faster with DatoCMS in Vue.js environments",
5
5
  "keywords": [
6
6
  "datocms",
@@ -11,7 +11,16 @@
11
11
  "main": "./dist/index.cjs.js",
12
12
  "module": "./dist/index.esm.mjs",
13
13
  "types": "./dist/index.d.ts",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git://github.com/datocms/vue-datocms.git"
17
+ },
14
18
  "license": "MIT",
19
+ "author": "Stefano Verna <s.verna@datocms.com>",
20
+ "contributors": [
21
+ "Silvano Stralla <silvano@datocms.com>"
22
+ ],
23
+ "homepage": "https://github.com/datocms/vue-datocms",
15
24
  "exports": {
16
25
  ".": {
17
26
  "import": "./dist/index.esm.mjs",