gallery.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. define([
  6. 'jquery',
  7. 'fotorama/fotorama',
  8. 'underscore',
  9. 'matchMedia',
  10. 'mage/template',
  11. 'text!mage/gallery/gallery.html',
  12. 'uiClass',
  13. 'mage/translate'
  14. ], function ($, fotorama, _, mediaCheck, template, galleryTpl, Class, $t) {
  15. 'use strict';
  16. /**
  17. * Retrieves index if the main item.
  18. * @param {Array.<Object>} data - Set of gallery items.
  19. */
  20. var getMainImageIndex = function (data) {
  21. var mainIndex;
  22. if (_.every(data, function (item) {
  23. return _.isObject(item);
  24. })
  25. ) {
  26. mainIndex = _.findIndex(data, function (item) {
  27. return item.isMain;
  28. });
  29. }
  30. return mainIndex > 0 ? mainIndex : 0;
  31. },
  32. /**
  33. * Helper for parse translate property
  34. *
  35. * @param {Element} el - el that to parse
  36. * @returns {Array} - array of properties.
  37. */
  38. getTranslate = function (el) {
  39. var slideTransform = $(el).attr('style').split(';');
  40. slideTransform = $.map(slideTransform, function (style) {
  41. style = style.trim();
  42. if (style.startsWith('transform: translate3d')) {
  43. return style.match(/transform: translate3d\((.+)px,(.+)px,(.+)px\)/);
  44. }
  45. return false;
  46. });
  47. return slideTransform.filter(Boolean);
  48. },
  49. /**
  50. * @param {*} str
  51. * @return {*}
  52. * @private
  53. */
  54. _toNumber = function (str) {
  55. var type = typeof str;
  56. if (type === 'string') {
  57. return parseInt(str); //eslint-disable-line radix
  58. }
  59. return str;
  60. };
  61. return Class.extend({
  62. defaults: {
  63. settings: {},
  64. config: {},
  65. startConfig: {}
  66. },
  67. /**
  68. * Checks if device has touch interface.
  69. * @return {Boolean} The result of searching touch events on device.
  70. */
  71. isTouchEnabled: (function () {
  72. return 'ontouchstart' in document.documentElement;
  73. })(),
  74. /**
  75. * Initializes gallery.
  76. * @param {Object} config - Gallery configuration.
  77. * @param {String} element - String selector of gallery DOM element.
  78. */
  79. initialize: function (config, element) {
  80. var self = this;
  81. this._super();
  82. _.bindAll(this,
  83. '_focusSwitcher'
  84. );
  85. /*turn off arrows for touch devices*/
  86. if (this.isTouchEnabled) {
  87. config.options.arrows = false;
  88. if (config.fullscreen) {
  89. config.fullscreen.arrows = false;
  90. }
  91. }
  92. config.options.width = _toNumber(config.options.width);
  93. config.options.height = _toNumber(config.options.height);
  94. config.options.thumbwidth = _toNumber(config.options.thumbwidth);
  95. config.options.thumbheight = _toNumber(config.options.thumbheight);
  96. config.options.swipe = true;
  97. this.config = config;
  98. this.settings = {
  99. $element: $(element),
  100. $pageWrapper: $('body>.page-wrapper'),
  101. currentConfig: config,
  102. defaultConfig: _.clone(config),
  103. fullscreenConfig: _.clone(config.fullscreen),
  104. breakpoints: config.breakpoints,
  105. activeBreakpoint: {},
  106. fotoramaApi: null,
  107. isFullscreen: false,
  108. api: null,
  109. data: _.clone(config.data)
  110. };
  111. config.options.ratio = config.options.width / config.options.height;
  112. config.options.height = null;
  113. $.extend(true, this.startConfig, config);
  114. this.initGallery();
  115. this.initApi();
  116. this.setupBreakpoints();
  117. this.initFullscreenSettings();
  118. this.settings.$element.on('mouseup', '.fotorama__stage__frame', function () {
  119. if (
  120. !$(this).parents('.fotorama__shadows--left, .fotorama__shadows--right').length &&
  121. !$(this).hasClass('fotorama-video-container')
  122. ) {
  123. self.openFullScreen();
  124. }
  125. });
  126. if (this.isTouchEnabled && this.settings.isFullscreen) {
  127. this.settings.$element.on('tap', '.fotorama__stage__frame', function () {
  128. var translate = getTranslate($(this).parents('.fotorama__stage__shaft'));
  129. if (translate[1] === '0' && !$(this).hasClass('fotorama-video-container')) {
  130. self.openFullScreen();
  131. self.settings.$pageWrapper.hide();
  132. }
  133. });
  134. }
  135. },
  136. /**
  137. * Open gallery fullscreen
  138. */
  139. openFullScreen: function () {
  140. this.settings.api.fotorama.requestFullScreen();
  141. this.settings.$fullscreenIcon.css({
  142. opacity: 1,
  143. visibility: 'visible',
  144. display: 'block'
  145. });
  146. },
  147. /**
  148. * Gallery fullscreen settings.
  149. */
  150. initFullscreenSettings: function () {
  151. var settings = this.settings,
  152. self = this;
  153. settings.$gallery = this.settings.$element.find('[data-gallery-role="gallery"]');
  154. settings.$fullscreenIcon = this.settings.$element.find('[data-gallery-role="fotorama__fullscreen-icon"]');
  155. settings.focusableStart = this.settings.$element.find('[data-gallery-role="fotorama__focusable-start"]');
  156. settings.focusableEnd = this.settings.$element.find('[data-gallery-role="fotorama__focusable-end"]');
  157. settings.closeIcon = this.settings.$element.find('[data-gallery-role="fotorama__fullscreen-icon"]');
  158. settings.fullscreenConfig.swipe = true;
  159. settings.$gallery.on('fotorama:fullscreenenter', function () {
  160. settings.closeIcon.show();
  161. settings.focusableStart.attr('tabindex', '0');
  162. settings.focusableEnd.attr('tabindex', '0');
  163. settings.focusableStart.bind('focusin', self._focusSwitcher);
  164. settings.focusableEnd.bind('focusin', self._focusSwitcher);
  165. settings.api.updateOptions(settings.defaultConfig.options, true);
  166. settings.api.updateOptions(settings.fullscreenConfig, true);
  167. if (!_.isEqual(settings.activeBreakpoint, {}) && settings.breakpoints) {
  168. settings.api.updateOptions(settings.activeBreakpoint.options, true);
  169. }
  170. settings.isFullscreen = true;
  171. });
  172. settings.$gallery.on('fotorama:fullscreenexit', function () {
  173. settings.closeIcon.hide();
  174. settings.focusableStart.attr('tabindex', '-1');
  175. settings.focusableEnd.attr('tabindex', '-1');
  176. settings.api.updateOptions(settings.defaultConfig.options, true);
  177. settings.focusableStart.unbind('focusin', this._focusSwitcher);
  178. settings.focusableEnd.unbind('focusin', this._focusSwitcher);
  179. settings.closeIcon.hide();
  180. if (!_.isEqual(settings.activeBreakpoint, {}) && settings.breakpoints) {
  181. settings.api.updateOptions(settings.activeBreakpoint.options, true);
  182. }
  183. settings.isFullscreen = false;
  184. settings.$element.data('gallery').updateOptions({
  185. swipe: true
  186. });
  187. });
  188. },
  189. /**
  190. * Switcher focus.
  191. */
  192. _focusSwitcher: function (e) {
  193. var target = $(e.target),
  194. settings = this.settings;
  195. if (target.is(settings.focusableStart)) {
  196. this._setFocus('start');
  197. } else if (target.is(settings.focusableEnd)) {
  198. this._setFocus('end');
  199. }
  200. },
  201. /**
  202. * Set focus to element.
  203. * @param {String} position - can be "start" and "end"
  204. * positions.
  205. * If position is "end" - sets focus to first
  206. * focusable element in modal window scope.
  207. * If position is "start" - sets focus to last
  208. * focusable element in modal window scope
  209. */
  210. _setFocus: function (position) {
  211. var settings = this.settings,
  212. focusableElements,
  213. infelicity;
  214. if (position === 'end') {
  215. settings.$gallery.find(settings.closeIcon).focus();
  216. } else if (position === 'start') {
  217. infelicity = 3; //Constant for find last focusable element
  218. focusableElements = settings.$gallery.find(':focusable');
  219. focusableElements.eq(focusableElements.length - infelicity).focus();
  220. }
  221. },
  222. /**
  223. * Initializes gallery with configuration options.
  224. */
  225. initGallery: function () {
  226. var breakpoints = {},
  227. settings = this.settings,
  228. config = this.config,
  229. tpl = template(galleryTpl, {
  230. next: $t('Next'),
  231. previous: $t('Previous')
  232. }),
  233. mainImageIndex;
  234. if (settings.breakpoints) {
  235. _.each(_.values(settings.breakpoints), function (breakpoint) {
  236. var conditions;
  237. _.each(_.pairs(breakpoint.conditions), function (pair) {
  238. conditions = conditions ? conditions + ' and (' + pair[0] + ': ' + pair[1] + ')' :
  239. '(' + pair[0] + ': ' + pair[1] + ')';
  240. });
  241. breakpoints[conditions] = breakpoint.options;
  242. });
  243. settings.breakpoints = breakpoints;
  244. }
  245. _.extend(config, config.options);
  246. config.options = undefined;
  247. config.click = false;
  248. config.breakpoints = null;
  249. settings.currentConfig = config;
  250. settings.$element.html(tpl);
  251. settings.$element.removeClass('_block-content-loading');
  252. settings.$elementF = $(settings.$element.children()[0]);
  253. settings.$elementF.fotorama(config);
  254. settings.fotoramaApi = settings.$elementF.data('fotorama');
  255. $.extend(true, config, this.startConfig);
  256. mainImageIndex = getMainImageIndex(config.data);
  257. if (mainImageIndex) {
  258. this.settings.fotoramaApi.show({
  259. index: mainImageIndex,
  260. time: 0
  261. });
  262. }
  263. },
  264. /**
  265. * Creates breakpoints for gallery.
  266. */
  267. setupBreakpoints: function () {
  268. var pairs,
  269. settings = this.settings,
  270. config = this.config,
  271. startConfig = this.startConfig,
  272. isTouchEnabled = this.isTouchEnabled;
  273. if (_.isObject(settings.breakpoints)) {
  274. pairs = _.pairs(settings.breakpoints);
  275. _.each(pairs, function (pair) {
  276. mediaCheck({
  277. media: pair[0],
  278. /**
  279. * Is triggered when breakpoint enties.
  280. */
  281. entry: function () {
  282. $.extend(true, config, _.clone(startConfig));
  283. settings.api.updateOptions(settings.defaultConfig.options, true);
  284. if (settings.isFullscreen) {
  285. settings.api.updateOptions(settings.fullscreenConfig, true);
  286. }
  287. if (isTouchEnabled) {
  288. settings.breakpoints[pair[0]].options.arrows = false;
  289. if (settings.breakpoints[pair[0]].options.fullscreen) {
  290. settings.breakpoints[pair[0]].options.fullscreen.arrows = false;
  291. }
  292. }
  293. settings.api.updateOptions(settings.breakpoints[pair[0]].options, true);
  294. $.extend(true, config, settings.breakpoints[pair[0]]);
  295. settings.activeBreakpoint = settings.breakpoints[pair[0]];
  296. },
  297. /**
  298. * Is triggered when breakpoint exits.
  299. */
  300. exit: function () {
  301. $.extend(true, config, _.clone(startConfig));
  302. settings.api.updateOptions(settings.defaultConfig.options, true);
  303. if (settings.isFullscreen) {
  304. settings.api.updateOptions(settings.fullscreenConfig, true);
  305. }
  306. settings.activeBreakpoint = {};
  307. }
  308. });
  309. });
  310. }
  311. },
  312. /**
  313. * Creates gallery's API.
  314. */
  315. initApi: function () {
  316. var settings = this.settings,
  317. config = this.config,
  318. api = {
  319. /**
  320. * Contains fotorama's API methods.
  321. */
  322. fotorama: settings.fotoramaApi,
  323. /**
  324. * Displays the last image on preview.
  325. */
  326. last: function () {
  327. settings.fotoramaApi.show('>>');
  328. },
  329. /**
  330. * Displays the first image on preview.
  331. */
  332. first: function () {
  333. settings.fotoramaApi.show('<<');
  334. },
  335. /**
  336. * Displays previous element on preview.
  337. */
  338. prev: function () {
  339. settings.fotoramaApi.show('<');
  340. },
  341. /**
  342. * Displays next element on preview.
  343. */
  344. next: function () {
  345. settings.fotoramaApi.show('>');
  346. },
  347. /**
  348. * Displays image with appropriate count number on preview.
  349. * @param {Number} index - Number of image that should be displayed.
  350. */
  351. seek: function (index) {
  352. if (_.isNumber(index) && index !== 0) {
  353. if (index > 0) {
  354. index -= 1;
  355. }
  356. settings.fotoramaApi.show(index);
  357. }
  358. },
  359. /**
  360. * Updates gallery with new set of options.
  361. * @param {Object} configuration - Standart gallery configuration object.
  362. * @param {Boolean} isInternal - Is this function called via breakpoints.
  363. */
  364. updateOptions: function (configuration, isInternal) {
  365. var $selectable = $('a[href], area[href], input, select, ' +
  366. 'textarea, button, iframe, object, embed, *[tabindex], *[contenteditable]')
  367. .not('[tabindex=-1], [disabled], :hidden'),
  368. $focus = $(':focus'),
  369. index;
  370. if (_.isObject(configuration)) {
  371. //Saves index of focus
  372. $selectable.each(function (number) {
  373. if ($(this).is($focus)) {
  374. index = number;
  375. }
  376. });
  377. if (this.isTouchEnabled) {
  378. configuration.arrows = false;
  379. }
  380. configuration.click = false;
  381. configuration.breakpoints = null;
  382. if (!isInternal) {
  383. !_.isEqual(settings.activeBreakpoint, {} && settings.brekpoints) ?
  384. $.extend(true, settings.activeBreakpoint.options, configuration) :
  385. settings.isFullscreen ?
  386. $.extend(true, settings.fullscreenConfig, configuration) :
  387. $.extend(true, settings.defaultConfig.options, configuration);
  388. }
  389. $.extend(true, settings.currentConfig.options, configuration);
  390. settings.fotoramaApi.setOptions(settings.currentConfig.options);
  391. if (_.isNumber(index)) {
  392. $selectable.eq(index).focus();
  393. }
  394. }
  395. },
  396. /**
  397. * Updates gallery with specific set of items.
  398. * @param {Array.<Object>} data - Set of gallery items to update.
  399. */
  400. updateData: function (data) {
  401. var mainImageIndex;
  402. if (_.isArray(data)) {
  403. settings.fotoramaApi.load(data);
  404. mainImageIndex = getMainImageIndex(data);
  405. if (mainImageIndex) {
  406. settings.fotoramaApi.show({
  407. index: mainImageIndex,
  408. time: 0
  409. });
  410. }
  411. $.extend(false, settings, {
  412. data: data,
  413. defaultConfig: data
  414. });
  415. $.extend(false, config, {
  416. data: data
  417. });
  418. }
  419. },
  420. /**
  421. * Returns current images list
  422. *
  423. * @returns {Array}
  424. */
  425. returnCurrentImages: function () {
  426. var images = [];
  427. _.each(this.fotorama.data, function (item) {
  428. images.push(_.omit(item, '$navThumbFrame', '$navDotFrame', '$stageFrame', 'labelledby'));
  429. });
  430. return images;
  431. },
  432. /**
  433. * Updates gallery data partially by index
  434. * @param {Number} index - Index of image in data array to be updated.
  435. * @param {Object} item - Standart gallery image object.
  436. *
  437. */
  438. updateDataByIndex: function (index, item) {
  439. settings.fotoramaApi.spliceByIndex(index, item);
  440. }
  441. };
  442. settings.$element.data('gallery', api);
  443. settings.api = settings.$element.data('gallery');
  444. settings.$element.trigger('gallery:loaded');
  445. }
  446. });
  447. });