workarea-jquery_zoom 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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));