viewerjs 1.12.0 → 1.13.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/src/js/methods.js CHANGED
@@ -41,9 +41,11 @@ import {
41
41
  getOffset,
42
42
  getPointersCenter,
43
43
  hasClass,
44
+ inheritAttributes,
44
45
  isFunction,
45
46
  isNumber,
46
47
  isPlainObject,
48
+ isTransitionEnabled,
47
49
  isUndefined,
48
50
  removeClass,
49
51
  removeListener,
@@ -98,7 +100,7 @@ export default {
98
100
  viewer.setAttribute('aria-modal', true);
99
101
  viewer.removeAttribute('aria-hidden');
100
102
 
101
- if (options.transition && !immediate) {
103
+ if (isTransitionEnabled(options, 'show') && !immediate) {
102
104
  const shown = this.shown.bind(this);
103
105
 
104
106
  this.transitioning = {
@@ -142,7 +144,7 @@ export default {
142
144
  });
143
145
  }
144
146
 
145
- if (dispatchEvent(element, EVENT_HIDE) === false) {
147
+ if (dispatchEvent(element, EVENT_HIDE) === false || this.destroyed) {
146
148
  return this;
147
149
  }
148
150
 
@@ -164,7 +166,7 @@ export default {
164
166
  this.hidden();
165
167
  };
166
168
 
167
- if (options.transition && !immediate) {
169
+ if (isTransitionEnabled(options, 'hide') && !immediate) {
168
170
  const onViewerTransitionEnd = (event) => {
169
171
  // Ignore all propagating `transitionend` events (#275).
170
172
  if (event && event.target === viewer) {
@@ -215,10 +217,12 @@ export default {
215
217
  * @returns {Viewer} this
216
218
  */
217
219
  view(index = this.options.initialViewIndex) {
220
+ const previousIndex = this.index;
221
+
218
222
  index = Number(index) || 0;
219
223
 
220
224
  if (this.hiding || this.played || index < 0 || index >= this.length
221
- || (this.viewed && index === this.index)) {
225
+ || (this.viewed && index === previousIndex)) {
222
226
  return this;
223
227
  }
224
228
 
@@ -237,20 +241,19 @@ export default {
237
241
  title,
238
242
  canvas,
239
243
  } = this;
240
- const item = this.items[index];
244
+ let item = this.getItem(index);
245
+
246
+ if (!item) {
247
+ this.initList(index);
248
+ item = this.getItem(index);
249
+ }
250
+
241
251
  const img = item.querySelector('img');
242
252
  const url = getData(img, 'originalUrl');
243
253
  const alt = img.getAttribute('alt');
244
254
  const image = document.createElement('img');
245
255
 
246
- forEach(options.inheritedAttributes, (name) => {
247
- const value = img.getAttribute(name);
248
-
249
- if (value !== null) {
250
- image.setAttribute(name, value);
251
- }
252
- });
253
-
256
+ inheritAttributes(image, img, options.inheritedAttributes);
254
257
  image.src = url;
255
258
  image.alt = alt;
256
259
 
@@ -268,7 +271,9 @@ export default {
268
271
  return this;
269
272
  }
270
273
 
271
- const activeItem = this.items[this.index];
274
+ this.hideMagnifier();
275
+
276
+ const activeItem = this.getItem(previousIndex);
272
277
 
273
278
  if (activeItem) {
274
279
  removeClass(activeItem, CLASS_ACTIVE);
@@ -309,6 +314,28 @@ export default {
309
314
  title.innerHTML = escapeHTMLEntities(isFunction(render)
310
315
  ? render.call(this, image, imageData)
311
316
  : `${alt} (${imageData.naturalWidth} × ${imageData.naturalHeight})`);
317
+
318
+ // Preload the image in the direction of navigation
319
+ if (options.preload) {
320
+ const direction = index < previousIndex ? -1 : 1;
321
+ let nextIndex = index + direction;
322
+
323
+ if (nextIndex < 0 || nextIndex >= this.length) {
324
+ if (options.loop) {
325
+ nextIndex = direction > 0 ? 0 : this.length - 1;
326
+ } else {
327
+ nextIndex = -1;
328
+ }
329
+ }
330
+
331
+ if (nextIndex !== index) {
332
+ const nextImage = this.images[nextIndex];
333
+ const preloadedImage = document.createElement('img');
334
+
335
+ inheritAttributes(preloadedImage, nextImage, options.inheritedAttributes);
336
+ preloadedImage.src = this.getImageURL(nextImage) || nextImage.src;
337
+ }
338
+ }
312
339
  };
313
340
  let onLoad;
314
341
  let onError;
@@ -482,6 +509,7 @@ export default {
482
509
  imageData.y = y;
483
510
  imageData.left = x;
484
511
  imageData.top = y;
512
+ toggleClass(this.image, CLASS_TRANSITION, isTransitionEnabled(options, 'move'));
485
513
  this.moving = true;
486
514
  this.renderImage(() => {
487
515
  this.moving = false;
@@ -546,6 +574,7 @@ export default {
546
574
  }
547
575
 
548
576
  imageData.rotate = degree;
577
+ toggleClass(this.image, CLASS_TRANSITION, isTransitionEnabled(options, 'rotate'));
549
578
  this.rotating = true;
550
579
  this.renderImage(() => {
551
580
  this.rotating = false;
@@ -637,6 +666,7 @@ export default {
637
666
 
638
667
  imageData.scaleX = scaleX;
639
668
  imageData.scaleY = scaleY;
669
+ toggleClass(this.image, CLASS_TRANSITION, isTransitionEnabled(options, 'scale'));
640
670
  this.scaling = true;
641
671
  this.renderImage(() => {
642
672
  this.scaling = false;
@@ -720,8 +750,12 @@ export default {
720
750
 
721
751
  if (isNumber(ratio) && this.viewed && !this.played && (_zoomable || options.zoomable)) {
722
752
  if (!_zoomable) {
723
- const minZoomRatio = Math.max(0.01, options.minZoomRatio);
724
- const maxZoomRatio = Math.min(100, options.maxZoomRatio);
753
+ const minZoomRatio = Math.max(0.01, isFunction(options.minZoomRatio)
754
+ ? options.minZoomRatio.call(this, this.image, imageData)
755
+ : options.minZoomRatio);
756
+ const maxZoomRatio = Math.min(100, isFunction(options.maxZoomRatio)
757
+ ? options.maxZoomRatio.call(this, this.image, imageData)
758
+ : options.maxZoomRatio);
725
759
 
726
760
  ratio = Math.min(Math.max(ratio, minZoomRatio), maxZoomRatio);
727
761
  }
@@ -795,6 +829,7 @@ export default {
795
829
  imageData.height = newHeight;
796
830
  imageData.oldRatio = oldRatio;
797
831
  imageData.ratio = ratio;
832
+ toggleClass(this.image, CLASS_TRANSITION, isTransitionEnabled(options, 'zoom'));
798
833
  this.renderImage(() => {
799
834
  this.zooming = false;
800
835
 
@@ -857,18 +892,17 @@ export default {
857
892
  }
858
893
 
859
894
  addClass(player, CLASS_SHOW);
860
- forEach(this.items, (item, i) => {
861
- const img = item.querySelector('img');
895
+ forEach(this.images, (originalImage, i) => {
862
896
  const image = document.createElement('img');
863
897
 
864
- image.src = getData(img, 'originalUrl');
865
- image.alt = img.getAttribute('alt');
866
- image.referrerPolicy = img.referrerPolicy;
898
+ image.src = this.getImageURL(originalImage) || originalImage.src;
899
+ image.alt = originalImage.alt || '';
900
+ image.referrerPolicy = originalImage.referrerPolicy;
867
901
  total += 1;
868
902
  addClass(image, CLASS_FADE);
869
- toggleClass(image, CLASS_TRANSITION, options.transition);
903
+ toggleClass(image, CLASS_TRANSITION, isTransitionEnabled(options, 'play'));
870
904
 
871
- if (hasClass(item, CLASS_ACTIVE)) {
905
+ if (i === this.index) {
872
906
  addClass(image, CLASS_IN);
873
907
  index = i;
874
908
  }
@@ -887,7 +921,7 @@ export default {
887
921
  index -= 1;
888
922
  index = index >= 0 ? index : total - 1;
889
923
  addClass(list[index], CLASS_IN);
890
- this.playing.timeout = setTimeout(prev, options.interval);
924
+ this.playing.timeout = options.autoplay ? setTimeout(prev, options.interval) : null;
891
925
  };
892
926
  const next = () => {
893
927
  clearTimeout(this.playing.timeout);
@@ -895,14 +929,14 @@ export default {
895
929
  index += 1;
896
930
  index = index < total ? index : 0;
897
931
  addClass(list[index], CLASS_IN);
898
- this.playing.timeout = setTimeout(next, options.interval);
932
+ this.playing.timeout = options.autoplay ? setTimeout(next, options.interval) : null;
899
933
  };
900
934
 
901
935
  if (total > 1) {
902
936
  this.playing = {
903
937
  prev,
904
938
  next,
905
- timeout: setTimeout(next, options.interval),
939
+ timeout: options.autoplay ? setTimeout(next, options.interval) : null,
906
940
  };
907
941
  }
908
942
  }
@@ -960,12 +994,10 @@ export default {
960
994
  this.open();
961
995
  addClass(this.button, CLASS_FULLSCREEN_EXIT);
962
996
 
963
- if (options.transition) {
964
- removeClass(list, CLASS_TRANSITION);
997
+ removeClass(list, CLASS_TRANSITION);
965
998
 
966
- if (this.viewed) {
967
- removeClass(image, CLASS_TRANSITION);
968
- }
999
+ if (this.viewed) {
1000
+ removeClass(image, CLASS_TRANSITION);
969
1001
  }
970
1002
 
971
1003
  addClass(viewer, CLASS_FIXED);
@@ -987,14 +1019,7 @@ export default {
987
1019
 
988
1020
  if (this.viewed) {
989
1021
  this.initImage(() => {
990
- this.renderImage(() => {
991
- if (options.transition) {
992
- setTimeout(() => {
993
- addClass(image, CLASS_TRANSITION);
994
- addClass(list, CLASS_TRANSITION);
995
- }, 0);
996
- }
997
- });
1022
+ this.renderImage();
998
1023
  });
999
1024
  }
1000
1025
 
@@ -1018,12 +1043,10 @@ export default {
1018
1043
  this.close();
1019
1044
  removeClass(this.button, CLASS_FULLSCREEN_EXIT);
1020
1045
 
1021
- if (options.transition) {
1022
- removeClass(list, CLASS_TRANSITION);
1046
+ removeClass(list, CLASS_TRANSITION);
1023
1047
 
1024
- if (this.viewed) {
1025
- removeClass(image, CLASS_TRANSITION);
1026
- }
1048
+ if (this.viewed) {
1049
+ removeClass(image, CLASS_TRANSITION);
1027
1050
  }
1028
1051
 
1029
1052
  if (options.focus) {
@@ -1044,14 +1067,7 @@ export default {
1044
1067
 
1045
1068
  if (this.viewed) {
1046
1069
  this.initImage(() => {
1047
- this.renderImage(() => {
1048
- if (options.transition) {
1049
- setTimeout(() => {
1050
- addClass(image, CLASS_TRANSITION);
1051
- addClass(list, CLASS_TRANSITION);
1052
- }, 0);
1053
- }
1054
- });
1070
+ this.renderImage();
1055
1071
  });
1056
1072
  }
1057
1073
 
@@ -1069,7 +1085,7 @@ export default {
1069
1085
  tooltipBox.textContent = `${Math.round(imageData.ratio * 100)}%`;
1070
1086
 
1071
1087
  if (!this.tooltipping) {
1072
- if (options.transition) {
1088
+ if (isTransitionEnabled(options, 'tooltip')) {
1073
1089
  if (this.fading) {
1074
1090
  dispatchEvent(tooltipBox, EVENT_TRANSITION_END);
1075
1091
  }
@@ -1091,7 +1107,7 @@ export default {
1091
1107
  }
1092
1108
 
1093
1109
  this.tooltipping = setTimeout(() => {
1094
- if (options.transition) {
1110
+ if (isTransitionEnabled(options, 'tooltip')) {
1095
1111
  addListener(tooltipBox, EVENT_TRANSITION_END, () => {
1096
1112
  removeClass(tooltipBox, CLASS_SHOW);
1097
1113
  removeClass(tooltipBox, CLASS_FADE);
@@ -1140,10 +1156,18 @@ export default {
1140
1156
  return this;
1141
1157
  },
1142
1158
 
1143
- // Update viewer when images changed
1144
- update() {
1159
+ /**
1160
+ * Update the viewer when images or options changed.
1161
+ * @param {Object} [updateOptions] - The options to update.
1162
+ * @returns {Viewer} this
1163
+ */
1164
+ update(updateOptions) {
1145
1165
  const { element, options, isImg } = this;
1146
1166
 
1167
+ if (isPlainObject(updateOptions)) {
1168
+ assign(options, updateOptions);
1169
+ }
1170
+
1147
1171
  // Destroy viewer if the target image was deleted
1148
1172
  if (isImg && !element.parentNode) {
1149
1173
  return this.destroy();
@@ -1171,9 +1195,10 @@ export default {
1171
1195
  if (this.ready) {
1172
1196
  const changedIndexes = [];
1173
1197
 
1174
- forEach(this.items, (item, i) => {
1198
+ forEach(this.items, (item) => {
1175
1199
  const img = item.querySelector('img');
1176
- const image = images[i];
1200
+ const index = Number(getData(item, 'index'));
1201
+ const image = images[index];
1177
1202
 
1178
1203
  if (image && img) {
1179
1204
  if (
@@ -1182,10 +1207,10 @@ export default {
1182
1207
  // Title changed (#408)
1183
1208
  || image.alt !== img.alt
1184
1209
  ) {
1185
- changedIndexes.push(i);
1210
+ changedIndexes.push(index);
1186
1211
  }
1187
1212
  } else {
1188
- changedIndexes.push(i);
1213
+ changedIndexes.push(index);
1189
1214
  }
1190
1215
  });
1191
1216
 
@@ -1204,7 +1229,7 @@ export default {
1204
1229
  this.viewed = false;
1205
1230
  this.view(Math.max(Math.min(this.index - changedIndex, this.length - 1), 0));
1206
1231
  } else {
1207
- const activeItem = this.items[this.index];
1232
+ const activeItem = this.getItem(this.index);
1208
1233
 
1209
1234
  // Reactivate the current viewing item after reset the list.
1210
1235
  addClass(activeItem, CLASS_ACTIVE);
package/src/js/others.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  ACTION_MOVE,
3
+ ACTION_ROTATE,
3
4
  ACTION_SWITCH,
5
+ ACTION_TRANSFORM,
4
6
  ACTION_ZOOM,
5
7
  CLASS_HIDE,
6
8
  CLASS_OPEN,
@@ -13,6 +15,7 @@ import {
13
15
  addListener,
14
16
  dispatchEvent,
15
17
  forEach,
18
+ getMaxRotateDegree,
16
19
  getMaxZoomRatio,
17
20
  isFunction,
18
21
  isPlainObject,
@@ -156,7 +159,7 @@ export default {
156
159
  },
157
160
 
158
161
  requestFullscreen(options) {
159
- const document = this.element.ownerDocument;
162
+ const { ownerDocument: document } = this;
160
163
 
161
164
  if (this.fulled && !(
162
165
  document.fullscreenElement
@@ -185,7 +188,7 @@ export default {
185
188
  },
186
189
 
187
190
  exitFullscreen() {
188
- const document = this.element.ownerDocument;
191
+ const { ownerDocument: document } = this;
189
192
 
190
193
  if (this.fulled && (
191
194
  document.fullscreenElement
@@ -229,7 +232,43 @@ export default {
229
232
 
230
233
  // Zoom the current image
231
234
  case ACTION_ZOOM:
232
- this.zoom(getMaxZoomRatio(pointers), false, null, event);
235
+ if (options.zoomable && options.zoomOnTouch) {
236
+ const zoomRatio = getMaxZoomRatio(pointers);
237
+
238
+ if (zoomRatio !== 0) {
239
+ this.zoom(zoomRatio, false, null, event);
240
+ }
241
+ }
242
+ break;
243
+
244
+ // Rotate the current image
245
+ case ACTION_ROTATE:
246
+ if (options.rotatable && options.rotateOnTouch) {
247
+ const rotateDegree = getMaxRotateDegree(pointers);
248
+
249
+ if (rotateDegree !== 0) {
250
+ this.rotate(rotateDegree, event);
251
+ }
252
+ }
253
+ break;
254
+
255
+ // Transform the current image
256
+ case ACTION_TRANSFORM:
257
+ if (options.zoomable && options.zoomOnTouch) {
258
+ const zoomRatio = getMaxZoomRatio(pointers);
259
+
260
+ if (zoomRatio !== 0) {
261
+ this.zoom(zoomRatio, false, null, event);
262
+ }
263
+ }
264
+
265
+ if (options.rotatable && options.rotateOnTouch) {
266
+ const rotateDegree = getMaxRotateDegree(pointers);
267
+
268
+ if (rotateDegree !== 0) {
269
+ this.rotate(rotateDegree, event);
270
+ }
271
+ }
233
272
  break;
234
273
 
235
274
  case ACTION_SWITCH: {
package/src/js/render.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ CLASS_ACTIVE,
2
3
  CLASS_LOADING,
3
4
  CLASS_TRANSITION,
4
5
  EVENT_ERROR,
@@ -11,11 +12,15 @@ import {
11
12
  addListener,
12
13
  assign,
13
14
  forEach,
15
+ getData,
14
16
  getImageNameFromURL,
15
17
  getImageNaturalSizes,
16
18
  getTransforms,
17
19
  hasClass,
20
+ inheritAttributes,
18
21
  isNumber,
22
+ isPlainObject,
23
+ isTransitionEnabled,
19
24
  removeClass,
20
25
  removeListener,
21
26
  setData,
@@ -31,7 +36,7 @@ export default {
31
36
  },
32
37
 
33
38
  initBody() {
34
- const { ownerDocument } = this.element;
39
+ const { ownerDocument } = this;
35
40
  const body = ownerDocument.body || ownerDocument.documentElement;
36
41
 
37
42
  this.body = body;
@@ -73,14 +78,41 @@ export default {
73
78
  }
74
79
  },
75
80
 
76
- initList() {
81
+ initList(viewIndex = this.index) {
77
82
  const { element, options, list } = this;
78
83
  const items = [];
84
+ const navbarOptions = isPlainObject(options.navbar) ? options.navbar : {};
85
+ const probe = document.createElement('li');
86
+
87
+ list.appendChild(probe);
88
+ const itemWidth = probe.offsetWidth + parseInt(window.getComputedStyle(probe).marginLeft, 10);
89
+
90
+ list.removeChild(probe);
91
+
92
+ let visibleItemCount = isNumber(navbarOptions.visibleItemCount)
93
+ ? Math.floor(navbarOptions.visibleItemCount)
94
+ : Math.floor(this.containerData.width / itemWidth);
95
+
96
+ visibleItemCount = Math.min(visibleItemCount, this.length);
97
+
98
+ const start = visibleItemCount > 0
99
+ ? Math.min(
100
+ Math.max(0, viewIndex - Math.floor(visibleItemCount / 2)),
101
+ Math.max(0, this.length - visibleItemCount),
102
+ )
103
+ : 0;
104
+ const end = visibleItemCount > 0
105
+ ? Math.min(this.length, start + visibleItemCount)
106
+ : this.length;
79
107
 
80
108
  // initList may be called in this.update, so should keep idempotent
81
109
  list.innerHTML = '';
82
110
 
83
111
  forEach(this.images, (image, index) => {
112
+ if (visibleItemCount > 0 && (index < start || index >= end)) {
113
+ return;
114
+ }
115
+
84
116
  const { src } = image;
85
117
  const alt = image.alt || getImageNameFromURL(src);
86
118
  const url = this.getImageURL(image);
@@ -89,13 +121,7 @@ export default {
89
121
  const item = document.createElement('li');
90
122
  const img = document.createElement('img');
91
123
 
92
- forEach(options.inheritedAttributes, (name) => {
93
- const value = image.getAttribute(name);
94
-
95
- if (value !== null) {
96
- img.setAttribute(name, value);
97
- }
98
- });
124
+ inheritAttributes(img, image, options.inheritedAttributes);
99
125
 
100
126
  if (options.navbar) {
101
127
  img.src = src || url;
@@ -119,6 +145,15 @@ export default {
119
145
 
120
146
  this.items = items;
121
147
 
148
+ if (this.viewed) {
149
+ const activeItem = this.getItem(this.index);
150
+
151
+ if (activeItem) {
152
+ addClass(activeItem, CLASS_ACTIVE);
153
+ activeItem.setAttribute('aria-selected', true);
154
+ }
155
+ }
156
+
122
157
  forEach(items, (item) => {
123
158
  const image = item.firstElementChild;
124
159
  let onLoad;
@@ -152,7 +187,7 @@ export default {
152
187
  });
153
188
  });
154
189
 
155
- if (options.transition) {
190
+ if (isTransitionEnabled(options, 'view')) {
156
191
  addListener(element, EVENT_VIEWED, () => {
157
192
  addClass(list, CLASS_TRANSITION);
158
193
  }, {
@@ -161,9 +196,24 @@ export default {
161
196
  }
162
197
  },
163
198
 
199
+ getItem(index) {
200
+ let item;
201
+
202
+ forEach(this.items, (candidate) => {
203
+ if (Number(getData(candidate, 'index')) === index) {
204
+ item = candidate;
205
+ return false;
206
+ }
207
+
208
+ return true;
209
+ });
210
+
211
+ return item;
212
+ },
213
+
164
214
  renderList() {
165
215
  const { index } = this;
166
- const item = this.items[index];
216
+ const item = this.getItem(index);
167
217
 
168
218
  if (!item) {
169
219
  return;
@@ -176,9 +226,9 @@ export default {
176
226
 
177
227
  // Place the active item in the center of the screen
178
228
  setStyle(this.list, assign({
179
- width: outerWidth * this.length - gutter,
229
+ width: outerWidth * this.items.length - gutter,
180
230
  }, getTransforms({
181
- translateX: ((this.viewerData.width - offsetWidth) / 2) - outerWidth * index,
231
+ translateX: ((this.viewerData.width - offsetWidth) / 2) - item.offsetLeft,
182
232
  })));
183
233
  },
184
234
 
@@ -277,9 +327,27 @@ export default {
277
327
  marginTop: imageData.y,
278
328
  }, getTransforms(imageData)));
279
329
 
330
+ if (this.magnifierPoint) {
331
+ this.renderMagnifier();
332
+ }
333
+
280
334
  if (done) {
281
- if ((this.viewing || this.moving || this.rotating || this.scaling || this.zooming)
282
- && this.options.transition
335
+ let action = false;
336
+
337
+ if (this.viewing) {
338
+ action = 'view';
339
+ } else if (this.moving) {
340
+ action = 'move';
341
+ } else if (this.rotating) {
342
+ action = 'rotate';
343
+ } else if (this.scaling) {
344
+ action = 'scale';
345
+ } else if (this.zooming) {
346
+ action = 'zoom';
347
+ }
348
+
349
+ if (action
350
+ && isTransitionEnabled(this.options, action)
283
351
  && hasClass(image, CLASS_TRANSITION)) {
284
352
  const onTransitionEnd = () => {
285
353
  this.imageRendering = false;
@@ -1,6 +1,13 @@
1
1
  export default (
2
2
  '<div class="viewer-container" tabindex="-1" touch-action="none">'
3
3
  + '<div class="viewer-canvas"></div>'
4
+ + '<div class="viewer-magnifier" aria-hidden="true">'
5
+ + '<img class="viewer-magnifier-image" alt="">'
6
+ + '</div>'
7
+ + '<div class="viewer-navigation">'
8
+ + '<div class="viewer-prev" data-viewer-action="prev" role="button" aria-label="Previous"></div>'
9
+ + '<div class="viewer-next" data-viewer-action="next" role="button" aria-label="Next"></div>'
10
+ + '</div>'
4
11
  + '<div class="viewer-footer">'
5
12
  + '<div class="viewer-title"></div>'
6
13
  + '<div class="viewer-toolbar"></div>'