handlers.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. import {
  2. ACTION_MOVE,
  3. ACTION_SWITCH,
  4. ACTION_ZOOM,
  5. CLASS_INVISIBLE,
  6. CLASS_LOADING,
  7. CLASS_MOVE,
  8. CLASS_TRANSITION,
  9. DATA_ACTION,
  10. EVENT_CLICK,
  11. EVENT_DBLCLICK,
  12. EVENT_LOAD,
  13. EVENT_VIEWED,
  14. IS_TOUCH_DEVICE,
  15. } from './constants';
  16. import {
  17. addClass,
  18. addListener,
  19. assign,
  20. dispatchEvent,
  21. forEach,
  22. getData,
  23. getImageNaturalSizes,
  24. getPointer,
  25. getTransforms,
  26. isFunction,
  27. isNumber,
  28. removeClass,
  29. setStyle,
  30. toggleClass,
  31. } from './utilities';
  32. export default {
  33. click(event) {
  34. const { target } = event;
  35. const { options, imageData } = this;
  36. const action = getData(target, DATA_ACTION);
  37. // Cancel the emulated click when the native click event was triggered.
  38. if (IS_TOUCH_DEVICE && event.isTrusted && target === this.canvas) {
  39. clearTimeout(this.clickCanvasTimeout);
  40. }
  41. switch (action) {
  42. case 'mix':
  43. if (this.played) {
  44. this.stop();
  45. } else if (options.inline) {
  46. if (this.fulled) {
  47. this.exit();
  48. } else {
  49. this.full();
  50. }
  51. } else {
  52. this.hide();
  53. }
  54. break;
  55. case 'hide':
  56. this.hide();
  57. break;
  58. case 'view':
  59. this.view(getData(target, 'index'));
  60. break;
  61. case 'zoom-in':
  62. this.zoom(0.1, true);
  63. break;
  64. case 'zoom-out':
  65. this.zoom(-0.1, true);
  66. break;
  67. case 'one-to-one':
  68. this.toggle();
  69. break;
  70. case 'reset':
  71. this.reset();
  72. break;
  73. case 'prev':
  74. this.prev(options.loop);
  75. break;
  76. case 'play':
  77. this.play(options.fullscreen);
  78. break;
  79. case 'next':
  80. this.next(options.loop);
  81. break;
  82. case 'rotate-left':
  83. this.rotate(-90);
  84. break;
  85. case 'rotate-right':
  86. this.rotate(90);
  87. break;
  88. case 'flip-horizontal':
  89. this.scaleX(-imageData.scaleX || -1);
  90. break;
  91. case 'flip-vertical':
  92. this.scaleY(-imageData.scaleY || -1);
  93. break;
  94. default:
  95. if (this.played) {
  96. this.stop();
  97. }
  98. }
  99. },
  100. dblclick(event) {
  101. event.preventDefault();
  102. if (this.viewed && event.target === this.image) {
  103. // Cancel the emulated double click when the native dblclick event was triggered.
  104. if (IS_TOUCH_DEVICE && event.isTrusted) {
  105. clearTimeout(this.doubleClickImageTimeout);
  106. }
  107. this.toggle();
  108. }
  109. },
  110. load() {
  111. if (this.timeout) {
  112. clearTimeout(this.timeout);
  113. this.timeout = false;
  114. }
  115. const {
  116. element,
  117. options,
  118. image,
  119. index,
  120. viewerData,
  121. } = this;
  122. removeClass(image, CLASS_INVISIBLE);
  123. if (options.loading) {
  124. removeClass(this.canvas, CLASS_LOADING);
  125. }
  126. image.style.cssText = (
  127. 'height:0;'
  128. + `margin-left:${viewerData.width / 2}px;`
  129. + `margin-top:${viewerData.height / 2}px;`
  130. + 'max-width:none!important;'
  131. + 'position:absolute;'
  132. + 'width:0;'
  133. );
  134. this.initImage(() => {
  135. toggleClass(image, CLASS_MOVE, options.movable);
  136. toggleClass(image, CLASS_TRANSITION, options.transition);
  137. this.renderImage(() => {
  138. this.viewed = true;
  139. this.viewing = false;
  140. if (isFunction(options.viewed)) {
  141. addListener(element, EVENT_VIEWED, options.viewed, {
  142. once: true,
  143. });
  144. }
  145. dispatchEvent(element, EVENT_VIEWED, {
  146. originalImage: this.images[index],
  147. index,
  148. image,
  149. });
  150. });
  151. });
  152. },
  153. loadImage(event) {
  154. const image = event.target;
  155. const parent = image.parentNode;
  156. const parentWidth = parent.offsetWidth || 30;
  157. const parentHeight = parent.offsetHeight || 50;
  158. const filled = !!getData(image, 'filled');
  159. getImageNaturalSizes(image, this.options, (naturalWidth, naturalHeight) => {
  160. const aspectRatio = naturalWidth / naturalHeight;
  161. let width = parentWidth;
  162. let height = parentHeight;
  163. if (parentHeight * aspectRatio > parentWidth) {
  164. if (filled) {
  165. width = parentHeight * aspectRatio;
  166. } else {
  167. height = parentWidth / aspectRatio;
  168. }
  169. } else if (filled) {
  170. height = parentWidth / aspectRatio;
  171. } else {
  172. width = parentHeight * aspectRatio;
  173. }
  174. setStyle(image, assign({
  175. width,
  176. height,
  177. }, getTransforms({
  178. translateX: (parentWidth - width) / 2,
  179. translateY: (parentHeight - height) / 2,
  180. })));
  181. });
  182. },
  183. keydown(event) {
  184. const { options } = this;
  185. if (!this.fulled || !options.keyboard) {
  186. return;
  187. }
  188. switch (event.keyCode || event.which || event.charCode) {
  189. // Escape
  190. case 27:
  191. if (this.played) {
  192. this.stop();
  193. } else if (options.inline) {
  194. if (this.fulled) {
  195. this.exit();
  196. }
  197. } else {
  198. this.hide();
  199. }
  200. break;
  201. // Space
  202. case 32:
  203. if (this.played) {
  204. this.stop();
  205. }
  206. break;
  207. // ArrowLeft
  208. case 37:
  209. this.prev(options.loop);
  210. break;
  211. // ArrowUp
  212. case 38:
  213. // Prevent scroll on Firefox
  214. event.preventDefault();
  215. // Zoom in
  216. this.zoom(options.zoomRatio, true);
  217. break;
  218. // ArrowRight
  219. case 39:
  220. this.next(options.loop);
  221. break;
  222. // ArrowDown
  223. case 40:
  224. // Prevent scroll on Firefox
  225. event.preventDefault();
  226. // Zoom out
  227. this.zoom(-options.zoomRatio, true);
  228. break;
  229. // Ctrl + 0
  230. case 48:
  231. // Fall through
  232. // Ctrl + 1
  233. // eslint-disable-next-line no-fallthrough
  234. case 49:
  235. if (event.ctrlKey) {
  236. event.preventDefault();
  237. this.toggle();
  238. }
  239. break;
  240. default:
  241. }
  242. },
  243. dragstart(event) {
  244. if (event.target.tagName.toLowerCase() === 'img') {
  245. event.preventDefault();
  246. }
  247. },
  248. pointerdown(event) {
  249. const { options, pointers } = this;
  250. const { buttons, button } = event;
  251. if (
  252. !this.viewed
  253. || this.showing
  254. || this.viewing
  255. || this.hiding
  256. // Handle mouse event and pointer event and ignore touch event
  257. || ((
  258. event.type === 'mousedown'
  259. || (event.type === 'pointerdown' && event.pointerType === 'mouse')
  260. ) && (
  261. // No primary button (Usually the left button)
  262. (isNumber(buttons) && buttons !== 1)
  263. || (isNumber(button) && button !== 0)
  264. // Open context menu
  265. || event.ctrlKey
  266. ))
  267. ) {
  268. return;
  269. }
  270. // Prevent default behaviours as page zooming in touch devices.
  271. event.preventDefault();
  272. if (event.changedTouches) {
  273. forEach(event.changedTouches, (touch) => {
  274. pointers[touch.identifier] = getPointer(touch);
  275. });
  276. } else {
  277. pointers[event.pointerId || 0] = getPointer(event);
  278. }
  279. let action = options.movable ? ACTION_MOVE : false;
  280. if (options.zoomOnTouch && options.zoomable && Object.keys(pointers).length > 1) {
  281. action = ACTION_ZOOM;
  282. } else if (options.slideOnTouch && (event.pointerType === 'touch' || event.type === 'touchstart') && this.isSwitchable()) {
  283. action = ACTION_SWITCH;
  284. }
  285. if (options.transition && (action === ACTION_MOVE || action === ACTION_ZOOM)) {
  286. removeClass(this.image, CLASS_TRANSITION);
  287. }
  288. this.action = action;
  289. },
  290. pointermove(event) {
  291. const { pointers, action } = this;
  292. if (!this.viewed || !action) {
  293. return;
  294. }
  295. event.preventDefault();
  296. if (event.changedTouches) {
  297. forEach(event.changedTouches, (touch) => {
  298. assign(pointers[touch.identifier] || {}, getPointer(touch, true));
  299. });
  300. } else {
  301. assign(pointers[event.pointerId || 0] || {}, getPointer(event, true));
  302. }
  303. this.change(event);
  304. },
  305. pointerup(event) {
  306. const { options, action, pointers } = this;
  307. let pointer;
  308. if (event.changedTouches) {
  309. forEach(event.changedTouches, (touch) => {
  310. pointer = pointers[touch.identifier];
  311. delete pointers[touch.identifier];
  312. });
  313. } else {
  314. pointer = pointers[event.pointerId || 0];
  315. delete pointers[event.pointerId || 0];
  316. }
  317. if (!action) {
  318. return;
  319. }
  320. event.preventDefault();
  321. if (options.transition && (action === ACTION_MOVE || action === ACTION_ZOOM)) {
  322. addClass(this.image, CLASS_TRANSITION);
  323. }
  324. this.action = false;
  325. // Emulate click and double click in touch devices to support backdrop and image zooming (#210).
  326. if (
  327. IS_TOUCH_DEVICE
  328. && action !== ACTION_ZOOM
  329. && pointer
  330. && (Date.now() - pointer.timeStamp < 500)
  331. ) {
  332. clearTimeout(this.clickCanvasTimeout);
  333. clearTimeout(this.doubleClickImageTimeout);
  334. if (options.toggleOnDblclick && this.viewed && event.target === this.image) {
  335. if (this.imageClicked) {
  336. this.imageClicked = false;
  337. // This timeout will be cleared later when a native dblclick event is triggering
  338. this.doubleClickImageTimeout = setTimeout(() => {
  339. dispatchEvent(this.image, EVENT_DBLCLICK);
  340. }, 50);
  341. } else {
  342. this.imageClicked = true;
  343. // The default timing of a double click in Windows is 500 ms
  344. this.doubleClickImageTimeout = setTimeout(() => {
  345. this.imageClicked = false;
  346. }, 500);
  347. }
  348. } else {
  349. this.imageClicked = false;
  350. if (options.backdrop && options.backdrop !== 'static' && event.target === this.canvas) {
  351. // This timeout will be cleared later when a native click event is triggering
  352. this.clickCanvasTimeout = setTimeout(() => {
  353. dispatchEvent(this.canvas, EVENT_CLICK);
  354. }, 50);
  355. }
  356. }
  357. }
  358. },
  359. resize() {
  360. if (!this.isShown || this.hiding) {
  361. return;
  362. }
  363. if (this.fulled) {
  364. this.close();
  365. this.initBody();
  366. this.open();
  367. }
  368. this.initContainer();
  369. this.initViewer();
  370. this.renderViewer();
  371. this.renderList();
  372. if (this.viewed) {
  373. this.initImage(() => {
  374. this.renderImage();
  375. });
  376. }
  377. if (this.played) {
  378. if (this.options.fullscreen && this.fulled && !(
  379. document.fullscreenElement
  380. || document.webkitFullscreenElement
  381. || document.mozFullScreenElement
  382. || document.msFullscreenElement
  383. )) {
  384. this.stop();
  385. return;
  386. }
  387. forEach(this.player.getElementsByTagName('img'), (image) => {
  388. addListener(image, EVENT_LOAD, this.loadImage.bind(this), {
  389. once: true,
  390. });
  391. dispatchEvent(image, EVENT_LOAD);
  392. });
  393. }
  394. },
  395. wheel(event) {
  396. if (!this.viewed) {
  397. return;
  398. }
  399. event.preventDefault();
  400. // Limit wheel speed to prevent zoom too fast
  401. if (this.wheeling) {
  402. return;
  403. }
  404. this.wheeling = true;
  405. setTimeout(() => {
  406. this.wheeling = false;
  407. }, 50);
  408. const ratio = Number(this.options.zoomRatio) || 0.1;
  409. let delta = 1;
  410. if (event.deltaY) {
  411. delta = event.deltaY > 0 ? 1 : -1;
  412. } else if (event.wheelDelta) {
  413. delta = -event.wheelDelta / 120;
  414. } else if (event.detail) {
  415. delta = event.detail > 0 ? 1 : -1;
  416. }
  417. this.zoom(-delta * ratio, true, event);
  418. },
  419. };