workarea-jquery_zoom 1.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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +20 -0
  3. data/.eslintrc.json +35 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  5. data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  7. data/.github/workflows/ci.yml +54 -0
  8. data/.gitignore +21 -0
  9. data/.rubocop.yml +2 -0
  10. data/CHANGELOG.md +1 -0
  11. data/CODE_OF_CONDUCT.md +3 -0
  12. data/CONTRIBUTING.md +3 -0
  13. data/Gemfile +6 -0
  14. data/LICENSE.md +3 -0
  15. data/README.md +100 -0
  16. data/Rakefile +109 -0
  17. data/app/assets/javascripts/jquery_zoom/jquery.zoom.js +610 -0
  18. data/bin/rails +20 -0
  19. data/config/initializers/appends.rb +7 -0
  20. data/config/initializers/workarea.rb +3 -0
  21. data/config/routes.rb +2 -0
  22. data/lib/tasks/jquery_zoom_tasks.rake +4 -0
  23. data/lib/workarea/jquery_zoom.rb +11 -0
  24. data/lib/workarea/jquery_zoom/engine.rb +9 -0
  25. data/lib/workarea/jquery_zoom/version.rb +5 -0
  26. data/package.json +9 -0
  27. data/test/dummy/Rakefile +6 -0
  28. data/test/dummy/app/assets/config/manifest.js +4 -0
  29. data/test/dummy/app/assets/images/.keep +0 -0
  30. data/test/dummy/app/assets/javascripts/application.js +13 -0
  31. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  32. data/test/dummy/app/controllers/application_controller.rb +3 -0
  33. data/test/dummy/app/controllers/concerns/.keep +0 -0
  34. data/test/dummy/app/helpers/application_helper.rb +2 -0
  35. data/test/dummy/app/jobs/application_job.rb +2 -0
  36. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  37. data/test/dummy/app/models/concerns/.keep +0 -0
  38. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  39. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  40. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  41. data/test/dummy/bin/bundle +3 -0
  42. data/test/dummy/bin/rails +4 -0
  43. data/test/dummy/bin/rake +4 -0
  44. data/test/dummy/bin/setup +38 -0
  45. data/test/dummy/bin/update +29 -0
  46. data/test/dummy/bin/yarn +11 -0
  47. data/test/dummy/config.ru +5 -0
  48. data/test/dummy/config/application.rb +28 -0
  49. data/test/dummy/config/boot.rb +5 -0
  50. data/test/dummy/config/cable.yml +10 -0
  51. data/test/dummy/config/environment.rb +5 -0
  52. data/test/dummy/config/environments/development.rb +54 -0
  53. data/test/dummy/config/environments/production.rb +91 -0
  54. data/test/dummy/config/environments/test.rb +44 -0
  55. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  56. data/test/dummy/config/initializers/assets.rb +14 -0
  57. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  58. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  59. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  60. data/test/dummy/config/initializers/inflections.rb +16 -0
  61. data/test/dummy/config/initializers/mime_types.rb +4 -0
  62. data/test/dummy/config/initializers/workarea.rb +5 -0
  63. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  64. data/test/dummy/config/locales/en.yml +33 -0
  65. data/test/dummy/config/puma.rb +56 -0
  66. data/test/dummy/config/routes.rb +5 -0
  67. data/test/dummy/config/secrets.yml +32 -0
  68. data/test/dummy/config/spring.rb +6 -0
  69. data/test/dummy/db/seeds.rb +2 -0
  70. data/test/dummy/lib/assets/.keep +0 -0
  71. data/test/dummy/log/.keep +0 -0
  72. data/test/dummy/package.json +5 -0
  73. data/test/teaspoon_env.rb +6 -0
  74. data/test/test_helper.rb +10 -0
  75. data/workarea-jquery_zoom.gemspec +22 -0
  76. data/yarn.lock +3290 -0
  77. metadata +147 -0
@@ -0,0 +1,610 @@
1
+ /**
2
+ * @author Jeremie Ges <jges@weblinc.com>
3
+ */
4
+ (function($) {
5
+ function Zoom() {
6
+
7
+ /**
8
+ * Cache DOM properties
9
+ * @type {Object}
10
+ */
11
+ this.$dom = {
12
+ container: null,
13
+ image: null,
14
+ thumbnail: null
15
+ },
16
+
17
+ /**
18
+ * Keep track of things
19
+ * @type {Object}
20
+ */
21
+ this.flags = {
22
+
23
+ /**
24
+ * The current scale
25
+ * @type {Number}
26
+ */
27
+ currentScale: 1,
28
+
29
+ /**
30
+ * Check if the zoom image is loaded
31
+ * @type {Boolean}
32
+ */
33
+ imageLoaded: false,
34
+
35
+ /**
36
+ * We use "transform: translate()" to "move" the
37
+ * zoom image (for smooth animations). When X or Y
38
+ * change, we update this property.
39
+ * @type {Object}
40
+ */
41
+ imageTranslate: {
42
+ x: 0,
43
+ y: 0
44
+ },
45
+
46
+ /**
47
+ * When the user starts to pinch, we keep track of the
48
+ * coordinates and "freeze" them until the pinch stops.
49
+ * Therefore the scale up / down is smoother.
50
+ * @type {Object}
51
+ */
52
+ pinchCoordinates: {
53
+ x: 0,
54
+ y: 0
55
+ },
56
+
57
+ /**
58
+ * Flag to know if we have to scale down or scale up
59
+ * @type {Number}
60
+ */
61
+ pinchScale: 0,
62
+
63
+ /**
64
+ * The Hammer js created instance (to be able to destroy it)
65
+ * @type {Object}
66
+ */
67
+ hammer: null
68
+ },
69
+
70
+ this.options = {},
71
+
72
+ /**
73
+ * Main entry of the widget
74
+ * @param {jQueryElement} container The scope
75
+ * @param {Object} options The options given by the user
76
+ */
77
+ this.init = function(container, options) {
78
+ this.$dom.container = $(container);
79
+ this.options = _.extend($.fn.zoom.defaults, options);
80
+ this.setup();
81
+ this.events();
82
+ },
83
+
84
+ /**
85
+ * Setup prerequisites before to listen
86
+ * the events
87
+ */
88
+ this.setup = function() {
89
+ this.setupImage();
90
+ this.setupThumbnail();
91
+ this.setupLoadImage();
92
+ },
93
+
94
+ /**
95
+ * Create a blank image where the zoom image
96
+ * will be stored
97
+ */
98
+ this.setupImage = function() {
99
+ this.$dom.image = $('<img/>');
100
+ },
101
+
102
+ /**
103
+ * Alias the $dom property thumbnail
104
+ * to the right image
105
+ */
106
+ this.setupThumbnail = function() {
107
+ this.$dom.thumbnail = this.$dom.container.find('img').first();
108
+ },
109
+
110
+ /**
111
+ * Will load the image directly if
112
+ * needed.
113
+ */
114
+ this.setupLoadImage = function() {
115
+ if (this.options.lazyLoad) {
116
+ return;
117
+ }
118
+
119
+ this.loadImage();
120
+ },
121
+
122
+ /**
123
+ * Start to listen the events.
124
+ */
125
+ this.events = function() {
126
+ this.$dom.image.on('load', this.onLoadImage.bind(this));
127
+
128
+ this.getInstanceHammer(this.$dom.container.get(0))
129
+ .on('doubletap', this.onDoubleTapContainer.bind(this))
130
+ .on('pan', this.onPanContainer.bind(this))
131
+ .on('pinchstart', this.onPinchStartContainer.bind(this))
132
+ .on('pinch', this.onPinchContainer.bind(this));
133
+
134
+ this.$dom.container.on('zoom.destroy', this.onDestroy.bind(this));
135
+
136
+ if (this.options.lazyLoad) {
137
+ this.$dom.container.on('click', this.onClickContainer.bind(this));
138
+ }
139
+ },
140
+
141
+ /**
142
+ * When the zoom image is loaded
143
+ */
144
+ this.onLoadImage = function() {
145
+ this.$dom.image
146
+ .css({
147
+ opacity: 1,
148
+ position: 'absolute',
149
+ top: 0,
150
+ left: 0,
151
+ width: this.$dom.container.width(),
152
+ height: this.$dom.container.outerHeight(),
153
+ border: 'none',
154
+ maxWidth: 'none',
155
+ maxHeight: 'none',
156
+ transformOrigin: '0 0',
157
+ transform: 'translate(0, 0) scale(1)',
158
+ transition: 'all 1s'
159
+ })
160
+ .attr('role', 'presentation')
161
+ .appendTo(this.$dom.container);
162
+
163
+ this.$dom.container.css('overflow', 'hidden');
164
+
165
+ this.flags.imageLoaded = true;
166
+ },
167
+
168
+ /**
169
+ * This callback is only called if the lazyLoad option is set to true.
170
+ * Click on the container will trigger the load.
171
+ */
172
+ this.onClickContainer = function() {
173
+ this.loadImage();
174
+ this.$dom.container.off('click');
175
+ },
176
+
177
+ /**
178
+ * When the user start to pan on the container
179
+ */
180
+ this.onPanContainer = function(e) {
181
+
182
+ var x = this.flags.imageTranslate.x,
183
+ y = this.flags.imageTranslate.y,
184
+ newX = x - (e.deltaX / 3),
185
+ newY = y - (e.deltaY / 3);
186
+
187
+ e.preventDefault();
188
+
189
+ if (!this.flags.imageLoaded) {
190
+ return;
191
+ }
192
+
193
+ if (newX > 0) {
194
+ newX = 0;
195
+ }
196
+
197
+ if (newY > 0) {
198
+ newY = 0;
199
+ }
200
+
201
+ if (newX < this.getPanLimits().x) {
202
+ newX = this.getPanLimits().x;
203
+ }
204
+
205
+ if (newY < this.getPanLimits().y) {
206
+ newY = this.getPanLimits().y;
207
+ }
208
+
209
+ this.$dom.image.css({
210
+ transition: 'all 0s'
211
+ });
212
+
213
+ this.updateImage(newX, newY);
214
+ },
215
+
216
+ /**
217
+ * When the user starts to pinch the container
218
+ * we want to keep track of the point clicked
219
+ * (coordinates) to scale up / down gracefully.
220
+ * @param {Event} e The pinch event
221
+ */
222
+ this.onPinchStartContainer = function(e) {
223
+ e.preventDefault();
224
+
225
+ if (!this.flags.imageLoaded) {
226
+ return;
227
+ }
228
+
229
+ this.$dom.image.css({
230
+ transition: 'all 1s'
231
+ });
232
+
233
+ this.flags.pinchCoordinates = e.center;
234
+ },
235
+
236
+ /**
237
+ * Guess if we have to scale up / down
238
+ * the zoom image on pinch
239
+ * @param {Event} e The pinch event
240
+ */
241
+ this.onPinchContainer = function(e) {
242
+ var scale = e.scale;
243
+
244
+ e.preventDefault();
245
+
246
+ if (!this.flags.imageLoaded) {
247
+ return;
248
+ }
249
+
250
+ if (scale < this.flags.pinchScale) {
251
+ this.onScaleDown();
252
+ } else {
253
+ this.onScaleUp();
254
+ }
255
+
256
+ this.flags.pinchScale = scale;
257
+ },
258
+
259
+ /**
260
+ * Scale down the zoom image around the point
261
+ * clicked by the user at the start of the pinch
262
+ */
263
+ this.onScaleDown = function() {
264
+
265
+ var scale = this.flags.currentScale,
266
+ containerOffset = this.$dom.container.offset(),
267
+ mousePositionOnImageX,
268
+ mousePositionOnImageY,
269
+ offsetX,
270
+ offsetY,
271
+ x,
272
+ y;
273
+
274
+ if (!this.flags.imageLoaded) {
275
+ return;
276
+ }
277
+
278
+ if (scale <= 1) {
279
+ return;
280
+ }
281
+
282
+ scale = scale - this.options.deltaScale;
283
+
284
+ mousePositionOnImageX = this.flags.pinchCoordinates.x - containerOffset.left;
285
+ mousePositionOnImageY = this.flags.pinchCoordinates.y - containerOffset.top;
286
+
287
+ offsetX = mousePositionOnImageX * this.options.deltaScale;
288
+ offsetY = mousePositionOnImageY * this.options.deltaScale;
289
+
290
+ x = this.flags.imageTranslate.x < 0 ? this.flags.imageTranslate.x : 0;
291
+ y = this.flags.imageTranslate.y < 0 ? this.flags.imageTranslate.y : 0;
292
+
293
+ offsetX = offsetX + x;
294
+ offsetY = offsetY + y;
295
+
296
+ if (scale <= 1) {
297
+ scale = 1;
298
+ offsetX = 0;
299
+ offsetY = 0;
300
+ }
301
+
302
+ this.updateImage(offsetX, offsetY, scale);
303
+ },
304
+
305
+ /**
306
+ * Scale up the zoom image around the point
307
+ * clicked by the user at the start of the pinch
308
+ */
309
+ this.onScaleUp = function() {
310
+
311
+ var scale = this.flags.currentScale + this.options.deltaScale,
312
+ containerOffset = this.$dom.container.offset(),
313
+ offsetX,
314
+ offsetY,
315
+ mousePositionOnImageX,
316
+ mousePositionOnImageY;
317
+
318
+ if (!this.flags.imageLoaded) {
319
+ return;
320
+ }
321
+
322
+ if (scale > this.getScaleLimitImage()) {
323
+ return;
324
+ }
325
+
326
+ mousePositionOnImageX = (this.flags.pinchCoordinates.x - containerOffset.left);
327
+ mousePositionOnImageY = (this.flags.pinchCoordinates.y - containerOffset.top);
328
+
329
+ offsetX = -(mousePositionOnImageX * this.options.deltaScale);
330
+ offsetY = -(mousePositionOnImageY * this.options.deltaScale);
331
+
332
+ offsetX = offsetX < 0 ? offsetX + this.flags.imageTranslate.x : 0;
333
+ offsetY = offsetY < 0 ? offsetY + this.flags.imageTranslate.y : 0;
334
+
335
+ this.updateImage(offsetX, offsetY, scale);
336
+ },
337
+
338
+ /**
339
+ * When the user double tap on the container,
340
+ * depending the current scale we zoom the image
341
+ * to its maximum or minimum
342
+ */
343
+ this.onDoubleTapContainer = function(e) {
344
+
345
+ var coordinates = e.center;
346
+
347
+ e.preventDefault();
348
+
349
+ if (!this.flags.imageLoaded) {
350
+ return;
351
+ }
352
+
353
+ this.$dom.image.css({
354
+ transition: 'all 1s'
355
+ });
356
+
357
+ if (this.flags.currentScale === 1) {
358
+ this.zoomMaximum(coordinates);
359
+ } else {
360
+ this.zoomMinimum();
361
+ }
362
+ },
363
+
364
+ /**
365
+ * Will scale up to the maximum scale allowed taking in account
366
+ * the focal point clicked by the user.
367
+ * @param {Object} coordinates - X / Y of the point clicked
368
+ */
369
+ this.zoomMaximum = function(coordinates) {
370
+ var maximumScale = this.getScaleLimitImage(),
371
+ containerOffset = this.$dom.container.offset(),
372
+ offsetX = -(coordinates.x * (maximumScale - this.flags.currentScale)),
373
+ offsetY = -(coordinates.y * (maximumScale - this.flags.currentScale));
374
+
375
+ this.updateImage(offsetX, offsetY, maximumScale);
376
+ },
377
+
378
+ /**
379
+ * Will scale down to scale 1
380
+ */
381
+ this.zoomMinimum = function() {
382
+ var x = 0,
383
+ y = 0,
384
+ minimumScale = 1;
385
+
386
+ this.updateImage(x, y, minimumScale);
387
+ },
388
+
389
+ /**
390
+ * Show the zoom image
391
+ */
392
+ this.showImage = function() {
393
+ this.$dom.image.css('opacity', 1);
394
+ },
395
+
396
+ /**
397
+ * Hide the zoom image
398
+ */
399
+ this.hideImage = function() {
400
+ this.$dom.image.css('opacity', 0);
401
+ },
402
+
403
+ /**
404
+ * Lazy load the image on demand.
405
+ */
406
+ this.loadImage = function() {
407
+ if (this.flags.imageLoaded) {
408
+ return;
409
+ }
410
+
411
+ this.$dom.image.attr('src', this.getUrlImage());
412
+ },
413
+
414
+ /**
415
+ * Apply x, y, scale to the zoom image
416
+ * @param {Number} x Translate to x
417
+ * @param {Number} y Translate to y
418
+ * @param {scale} scale The scale to apply
419
+ */
420
+ this.updateImage = function(x, y, scale) {
421
+ scale = scale || this.flags.currentScale;
422
+
423
+ // Let's be nice with the browser and give him
424
+ // rounded values.
425
+ x = Math.round(x);
426
+ y = Math.round(y);
427
+
428
+ this.$dom.image.css({
429
+ transform: this.getCssRuleTranslate(x, y) + ' ' + this.getCssRuleScale(scale)
430
+ });
431
+
432
+ // Keep track of transformations
433
+ this.flags.imageTranslate.y = y;
434
+ this.flags.imageTranslate.x = x;
435
+ this.flags.currentScale = scale;
436
+ }
437
+
438
+ /**
439
+ * Get the url of the zoom image to use.
440
+ * @return {String} Url (relative or absolute)
441
+ */
442
+ this.getUrlImage = function() {
443
+ var url = this.options.url;
444
+
445
+ if (!_.isEmpty(url)) {
446
+ return url;
447
+ }
448
+
449
+ // Let's find by the attribute
450
+ return this.$dom.container.data('zoom-src');
451
+ },
452
+
453
+ /**
454
+ * When the zoom image is scaling up, we need to know
455
+ * the limit of scaling to keep the perfect quality ratio.
456
+ * @return {Float} The scale up limit
457
+ */
458
+ this.getScaleLimitImage = function() {
459
+ var image = this.getNaturalDimensionsImage(),
460
+ scaleWidth,
461
+ scaleHeight,
462
+ limit;
463
+
464
+ scaleWidth = image.width / this.$dom.container.width();
465
+ scaleHeight = image.height / this.$dom.container.outerHeight();
466
+
467
+ limit = _.min([scaleWidth, scaleHeight]);
468
+
469
+ return _.round(limit, 2);
470
+ },
471
+
472
+ /**
473
+ * When the zoom image is panning (up / down / left / right),
474
+ * we need to know what are the limits for X and Y to avoid
475
+ * to pan outside of the container.
476
+ * @return {Object} The X / Y coordinates limits
477
+ */
478
+ this.getPanLimits = function() {
479
+ var xLimit = (this.$dom.image.width() * this.flags.currentScale) - this.$dom.container.width(),
480
+ yLimit = (this.$dom.image.height() * this.flags.currentScale) - this.$dom.container.outerHeight();
481
+
482
+ return {
483
+ x: -xLimit,
484
+ y: -yLimit
485
+ }
486
+ },
487
+
488
+ /**
489
+ * Get the real width / height of the thumbnail
490
+ * @return {Object} The width / height
491
+ */
492
+ this.getNaturalDimensionsThumbnail = function() {
493
+ return {
494
+ width: this.$dom.thumbnail.prop('naturalWidth'),
495
+ height: this.$dom.thumbnail.prop('naturalHeight')
496
+ }
497
+ },
498
+
499
+ /**
500
+ * Get the real width / height of the zoom image
501
+ * @return {Object} The width / height
502
+ */
503
+ this.getNaturalDimensionsImage = function() {
504
+ return {
505
+ width: this.$dom.image.prop('naturalWidth'),
506
+ height: this.$dom.image.prop('naturalHeight')
507
+ }
508
+ },
509
+
510
+ /**
511
+ * Abstraction to clean up the code.
512
+ * @param {Mixed} x The X coordinates
513
+ * @param {Mixed} y The Y coordinates
514
+ * @return {String} The css translate rule for the transform property
515
+ */
516
+ this.getCssRuleTranslate = function(x, y) {
517
+ return 'translate(' + x + 'px,' + y + 'px)';
518
+ },
519
+
520
+ /**
521
+ * Abstraction to clean up the code.
522
+ * @param {Mixed} scale The scale
523
+ * @return {String} The css scale rule for the transform property
524
+ */
525
+ this.getCssRuleScale = function(scale) {
526
+ return 'scale(' + scale + ')';
527
+ },
528
+
529
+ /**
530
+ * Create an hammer instance for the
531
+ * element given with the right recognizers:
532
+ * Double Tap / Pinch / Pan
533
+ * @param {HTMLelement} element - Initialize the events to this element
534
+ *
535
+ * @example
536
+ * var element = document.getElementById('element');
537
+ * this.getInstanceHammer(element);
538
+ */
539
+ this.getInstanceHammer = function(element) {
540
+ var manager = new Hammer.Manager(element),
541
+ doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2}),
542
+ pinch = new Hammer.Pinch(),
543
+ pan = new Hammer.Pan({threshold: 0});
544
+
545
+ manager.add([doubleTap, pinch, pan]);
546
+
547
+ this.flags.hammer = manager;
548
+
549
+ return manager;
550
+ },
551
+
552
+ /**
553
+ * Destroy the widget
554
+ */
555
+ this.onDestroy = function() {
556
+
557
+ // Shutdown events
558
+ this.$dom.image.off('load');
559
+ this.flags.hammer.off('doubletap pan pinchstart pinch');
560
+ this.$dom.container.off('zoom.destroy');
561
+ this.$dom.container.off('click');
562
+
563
+ // Remove added DOM
564
+ this.$dom.image.remove();
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Public jQuery API
570
+ */
571
+
572
+ $.fn.zoom = function(options) {
573
+
574
+ var options = options || {};
575
+
576
+ return this.each(function() {
577
+ new Zoom().init(this, options);
578
+ });
579
+ };
580
+
581
+ $.fn.zoom.defaults = {
582
+
583
+ /**
584
+ * Do you want to lazy load the zoom image?
585
+ * We will load the zoom image when the user clicks
586
+ * one time on the container.
587
+ * @type {Boolean}
588
+ */
589
+ lazyLoad: true,
590
+
591
+ /**
592
+ * What is the increment scale you want to use
593
+ * when scale up / down.
594
+ *
595
+ * @example
596
+ * 1 -> 1.05 -> 1.10 -> ..
597
+ *
598
+ * @type {Number}
599
+ */
600
+ deltaScale: 0.05,
601
+
602
+ /**
603
+ * The url of the zoom image, if not defined, the plugin
604
+ * will fetch the attribute "data-zoom-src" given.
605
+ * @type {Mixed}
606
+ */
607
+ url: null
608
+ };
609
+
610
+ }(window.jQuery));