zoom.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. /**
  6. * @deprecated since version 2.2.0
  7. */
  8. (function (root, factory) {
  9. 'use strict';
  10. if (typeof define === 'function' && define.amd) {
  11. define([
  12. 'jquery',
  13. 'mage/template',
  14. 'jquery/ui'
  15. ], factory);
  16. } else {
  17. factory(root.jQuery, root.mageTemplate);
  18. }
  19. }(this, function ($, mageTemplate) {
  20. 'use strict';
  21. $.widget('mage.zoom', {
  22. options: {
  23. largeImage: null,
  24. startZoomEvent: 'click',
  25. stopZoomEvent: 'mouseleave',
  26. hideDelay: '100',
  27. effects: {
  28. show: {
  29. effect: 'fade',
  30. duration: 100
  31. },
  32. hide: {
  33. effect: 'fade',
  34. duration: 100
  35. }
  36. },
  37. controls: {
  38. lens: {
  39. template: '[data-template=zoom-lens]',
  40. opacity: 0.7,
  41. background: '#ffffff'
  42. },
  43. track: {
  44. template: '[data-template=zoom-track]'
  45. },
  46. display: {
  47. template: '[data-template=zoom-display]',
  48. width: 400,
  49. height: 400,
  50. left: 0,
  51. top: 0
  52. },
  53. notice: {
  54. template: '[data-template=notice]',
  55. text: null,
  56. container: '[data-role=gallery-notice-container]'
  57. }
  58. },
  59. selectors: {
  60. image: '[data-role=zoom-image]',
  61. imageContainer: '[data-role=gallery-base-image-container]',
  62. zoomInner: '[data-role=zoom-inner]',
  63. track: '[data-role=zoom-track]',
  64. notice: '[data-role=notice]'
  65. }
  66. },
  67. noticeOriginal: '',
  68. /**
  69. * Widget constructor.
  70. * @protected
  71. */
  72. _create: function () {
  73. this._setZoomData();
  74. this._render();
  75. this._bind();
  76. if (this.largeImage[0].complete) {
  77. this._largeImageLoaded();
  78. }
  79. this._hide(this.display);
  80. this._hide(this.track);
  81. },
  82. /**
  83. * Render zoom controls.
  84. * @protected
  85. */
  86. _render: function () {
  87. var noticeContainer;
  88. this.element.append(this._renderControl('track').append(this._renderControl('lens')));
  89. this.element.append(this._renderControl('display'))
  90. .find(this.options.selectors.zoomInner)
  91. .append(this._renderLargeImage());
  92. noticeContainer = this.element.find(this.options.controls.notice.container);
  93. noticeContainer = noticeContainer.length ?
  94. noticeContainer :
  95. this.element;
  96. noticeContainer.append(this._renderControl('notice'));
  97. },
  98. /**
  99. * Toggle zoom notice.
  100. * @protected
  101. */
  102. _toggleNotice: function () {
  103. this.noticeOriginal = this.notice.text() !== this.options.controls.notice.text ?
  104. this.notice.text() :
  105. this.noticeOriginal;
  106. if (this.getZoomRatio() > 1 && this.largeImageSrc && !this.activated) {
  107. this.notice.text(this.options.controls.notice.text);
  108. } else {
  109. this.notice.text(this.noticeOriginal);
  110. }
  111. },
  112. /**
  113. * Render zoom control.
  114. *
  115. * @param {String} control - name of the control
  116. * @return {Element} DOM-element
  117. * @protected
  118. */
  119. _renderControl: function (control) {
  120. var controlData = this.options.controls[control],
  121. templateData = {},
  122. css = {},
  123. controlElement;
  124. switch (control) {
  125. case 'display':
  126. templateData = {
  127. img: this.largeImageSrc
  128. };
  129. css = {
  130. width: controlData.width,
  131. height: controlData.height
  132. };
  133. break;
  134. case 'notice':
  135. templateData = {
  136. text: controlData.text || ''
  137. };
  138. break;
  139. }
  140. controlElement = this.element.find(this.options.selectors[control]);
  141. controlElement = controlElement.length ?
  142. controlElement :
  143. $(mageTemplate(controlData.template, {
  144. data: templateData
  145. }));
  146. this[control] = controlElement.css(css);
  147. return this[control];
  148. },
  149. /**
  150. * Refresh zoom controls.
  151. * @protected
  152. */
  153. _refresh: function () {
  154. this._refreshControl('display');
  155. this._refreshControl('track');
  156. this._refreshControl('lens');
  157. },
  158. /**
  159. * Refresh zoom control position and css.
  160. *
  161. * @param {String} control - name of the control
  162. * @protected
  163. */
  164. _refreshControl: function (control) {
  165. var controlData = this.options.controls[control],
  166. position,
  167. css = {
  168. position: 'absolute'
  169. };
  170. switch (control) {
  171. case 'display':
  172. position = {
  173. my: 'left+' + this.options.controls.display.left + ' top+' +
  174. this.options.controls.display.top + '',
  175. at: 'left+' + $(this.image).outerWidth() + ' top',
  176. of: $(this.image)
  177. };
  178. break;
  179. case 'track':
  180. $.extend(css, {
  181. height: $(this.image).height(),
  182. width: $(this.image).width()
  183. });
  184. position = {
  185. my: 'left top',
  186. at: 'left top',
  187. of: $(this.image)
  188. };
  189. break;
  190. case 'lens':
  191. $.extend(css, this._calculateLensSize(), {
  192. background: controlData.background,
  193. opacity: controlData.opacity,
  194. left: 0,
  195. top: 0
  196. });
  197. break;
  198. }
  199. this[control].css(css);
  200. if (position) {
  201. this[control].position(position);
  202. }
  203. },
  204. /**
  205. * Bind zoom event handlers.
  206. * @protected
  207. */
  208. _bind: function () {
  209. /* Events delegated to this.element, which means that all zoom controls can be changed any time
  210. * and not required to re-bind events
  211. */
  212. var events = {};
  213. events[this.options.startZoomEvent + ' ' + this.options.selectors.image] = 'show';
  214. /** Handler */
  215. events[this.options.stopZoomEvent + ' ' + this.options.selectors.track] = function () {
  216. this._delay(this.hide, this.options.hideDelay || 0);
  217. };
  218. events['mousemove ' + this.options.selectors.track] = '_move';
  219. events.imageupdated = '_onImageUpdated';
  220. this._on(events);
  221. this._on(this.largeImage, {
  222. load: '_largeImageLoaded'
  223. });
  224. },
  225. /**
  226. * Store initial zoom data.
  227. * @protected
  228. */
  229. _setZoomData: function () {
  230. this.image = this.element.find(this.options.selectors.image);
  231. this.largeImageSrc = this.options.largeImage ||
  232. this.element.find(this.image).data('large');
  233. },
  234. /**
  235. * Update zoom when called enable method.
  236. * @override
  237. */
  238. enable: function () {
  239. this._super();
  240. this._onImageUpdated();
  241. },
  242. /**
  243. * Toggle notice when called disable method.
  244. * @override
  245. */
  246. disable: function () {
  247. this.notice.text(this.noticeOriginal || '');
  248. this._super();
  249. },
  250. /**
  251. * Show zoom controls.
  252. *
  253. * @param {Object} e - event object
  254. */
  255. show: function (e) {
  256. e.preventDefault();
  257. if (this.getZoomRatio() > 1 && this.largeImageSrc) {
  258. e.stopImmediatePropagation();
  259. this.activated = true;
  260. this._show(this.display, this.options.effects.show);
  261. this._show(this.track, this.options.effects.show);
  262. this._refresh();
  263. this.lens.position({
  264. my: 'center',
  265. at: 'center',
  266. of: e,
  267. using: $.proxy(this._refreshZoom, this)
  268. });
  269. this._toggleNotice();
  270. this._trigger('show');
  271. }
  272. },
  273. /** Hide zoom controls */
  274. hide: function () {
  275. this.activated = false;
  276. this._hide(this.display, this.options.effects.hide);
  277. this._hide(this.track, this.options.effects.hide);
  278. this._toggleNotice();
  279. this._trigger('hide');
  280. },
  281. /**
  282. * Refresh zoom when image is updated
  283. * @protected
  284. */
  285. _onImageUpdated: function () {
  286. // Stop loader in case previous active image has not been loaded yet
  287. $(this.options.selectors.image).trigger('processStop');
  288. if (!this.image.is($(this.options.selectors.image))) {
  289. this._setZoomData();
  290. if (this.largeImageSrc) {
  291. this._refreshLargeImage();
  292. this._refresh();
  293. } else {
  294. this.hide();
  295. }
  296. }
  297. },
  298. /**
  299. * Reset this.ratio when large image is loaded
  300. * @protected
  301. */
  302. _largeImageLoaded: function () {
  303. this.largeImage.css({
  304. width: 'auto',
  305. height: 'auto'
  306. });
  307. this.largeImageSize = {
  308. width: this.largeImage.width() || this.largeImage.get(0).naturalWidth,
  309. height: this.largeImage.height() || this.largeImage.get(0).naturalHeight
  310. };
  311. this.ratio = null;
  312. this._toggleNotice();
  313. $(this.options.selectors.image).trigger('processStop');
  314. },
  315. /**
  316. * Refresh large image (refresh "src" and initial position)
  317. * @protected
  318. */
  319. _refreshLargeImage: function () {
  320. var oldSrc;
  321. if (this.largeImage) {
  322. oldSrc = this.largeImage.attr('src');
  323. if (oldSrc !== this.largeImageSrc) {
  324. $(this.options.selectors.image).trigger('processStart');
  325. this.largeImage.attr('src', this.largeImageSrc);
  326. }
  327. this.largeImage.css({
  328. top: 0,
  329. left: 0
  330. });
  331. }
  332. },
  333. /**
  334. * @return {Element} DOM-element
  335. * @protected
  336. */
  337. _renderLargeImage: function () {
  338. var image = $(this.options.selectors.image);
  339. // Start loader if 'load' event of image is expected to trigger later
  340. if (this.largeImageSrc) {
  341. image.trigger('processStart');
  342. }
  343. // No need to create template just for img tag
  344. this.largeImage = $('<img />', {
  345. src: this.largeImageSrc
  346. });
  347. return this.largeImage;
  348. },
  349. /**
  350. * Calculate zoom ratio.
  351. *
  352. * @return {Number}
  353. * @protected
  354. */
  355. getZoomRatio: function () {
  356. var imageWidth;
  357. if (this.ratio === null || typeof this.ratio === 'undefined') {
  358. imageWidth = $(this.image).width() || $(this.image).prop('width');
  359. return this.largeImageSize ? this.largeImageSize.width / imageWidth : 1;
  360. }
  361. return this.ratio;
  362. },
  363. /**
  364. * Calculate lens size, depending on zoom ratio.
  365. *
  366. * @return {Object} object contain width and height fields
  367. * @protected
  368. */
  369. _calculateLensSize: function () {
  370. var displayData = this.options.controls.display,
  371. ratio = this.getZoomRatio();
  372. return {
  373. width: Math.ceil(displayData.width / ratio),
  374. height: Math.ceil(displayData.height / ratio)
  375. };
  376. },
  377. /**
  378. * Refresh position of large image depending of position of zoom lens.
  379. *
  380. * @param {Object} position
  381. * @param {Object} ui
  382. * @protected
  383. */
  384. _refreshZoom: function (position, ui) {
  385. $(ui.element.element).css(position);
  386. this.largeImage.css(this._getLargeImageOffset(position));
  387. },
  388. /**
  389. * @param {Object} position
  390. * @return {Object}
  391. * @private
  392. */
  393. _getLargeImageOffset: function (position) {
  394. var ratio = this.getZoomRatio();
  395. return {
  396. top: -(position.top * ratio),
  397. left: -(position.left * ratio)
  398. };
  399. },
  400. /**
  401. * Mouse move handler.
  402. *
  403. * @param {Object} e - event object
  404. * @protected
  405. */
  406. _move: function (e) {
  407. this.lens.position({
  408. my: 'center',
  409. at: 'left top',
  410. of: e,
  411. collision: 'fit',
  412. within: this.image,
  413. using: $.proxy(this._refreshZoom, this)
  414. });
  415. }
  416. });
  417. /** Extension for zoom widget - white borders detection */
  418. $.widget('mage.zoom', $.mage.zoom, {
  419. /**
  420. * Get aspect ratio of the element.
  421. *
  422. * @param {Object} element - jQuery collection
  423. * @return {*}
  424. * @protected
  425. */
  426. _getAspectRatio: function (element) {
  427. var width, height, aspectRatio;
  428. if (!element || !element.length) {
  429. return null;
  430. }
  431. width = element.width() || element.prop('width');
  432. height = element.height() || element.prop('height');
  433. aspectRatio = width / height;
  434. return Math.round(aspectRatio * 100) / 100;
  435. },
  436. /**
  437. * Calculate large image offset depending on enabled "white borders" functionality.
  438. *
  439. * @return {Object}
  440. * @protected
  441. */
  442. _getWhiteBordersOffset: function () {
  443. var ratio = this.getZoomRatio(),
  444. largeWidth = this.largeImageSize.width / ratio,
  445. largeHeight = this.largeImageSize.height / ratio,
  446. width = this.image.width() || this.image.prop('width'),
  447. height = this.image.height() || this.image.prop('height'),
  448. offsetLeft = width - largeWidth > 0 ?
  449. Math.ceil((width - largeWidth) / 2) :
  450. 0,
  451. offsetTop = height - largeHeight > 0 ?
  452. Math.ceil((height - largeHeight) / 2) :
  453. 0;
  454. return {
  455. top: offsetTop,
  456. left: offsetLeft
  457. };
  458. },
  459. /**
  460. * @override
  461. */
  462. _largeImageLoaded: function () {
  463. this._super();
  464. this.whiteBordersOffset = null;
  465. if (this._getAspectRatio(this.image) !== this._getAspectRatio(this.largeImage)) {
  466. this.whiteBordersOffset = this._getWhiteBordersOffset();
  467. }
  468. },
  469. /**
  470. * @override
  471. */
  472. _getLargeImageOffset: function (position) {
  473. if (this.whiteBordersOffset) {
  474. position.top -= this.whiteBordersOffset.top;
  475. position.left -= this.whiteBordersOffset.left;
  476. }
  477. return this._superApply([position]);
  478. }
  479. });
  480. return $.mage.zoom;
  481. }));