Magnifier.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. /**
  2. * Magnifier.js is a Javascript library enabling magnifying glass effect on an images.
  3. *
  4. * Features
  5. *
  6. * Zoom in / out functionality using mouse wheel
  7. * Setting options via Javascript or data attributes
  8. * Magnified image can be displayed in the lens itself or outside of it in a wrapper
  9. * Attachment to multiple images with single call
  10. * Attachment of user defined functions for thumbnail entering, moving and leaving and image zooming events
  11. * Display loading text while the large image is being loaded, and switch to lens once its loaded
  12. *
  13. * Magnifier.js uses Event.js as a cross-browser event handling wrapper, which is available at
  14. * Github and JSClasses.org:
  15. *
  16. * Github - https://github.com/mark-rolich/Event.js
  17. * JS Classes - http://www.jsclasses.org/package/212-JavaScript-Handle-events-in-a-browser-independent-manner.html
  18. *
  19. * Works in Chrome, Firefox, Safari, IE 7, 8, 9 & 10.
  20. *
  21. * @author Mark Rolich <mark.rolich@gmail.com>
  22. */
  23. var Magnifier = function (evt, options) {
  24. "use strict";
  25. var gOptions = options || {},
  26. curThumb = null,
  27. curData = {
  28. x: 0,
  29. y: 0,
  30. w: 0,
  31. h: 0,
  32. lensW: 0,
  33. lensH: 0,
  34. lensBgX: 0,
  35. lensBgY: 0,
  36. largeW: 0,
  37. largeH: 0,
  38. largeL: 0,
  39. largeT: 0,
  40. zoom: 2,
  41. zoomMin: 1.1,
  42. zoomMax: 5,
  43. mode: 'outside',
  44. largeWrapperId: (gOptions.largeWrapper !== undefined)
  45. ? (gOptions.largeWrapper.id || null)
  46. : null,
  47. status: 0,
  48. zoomAttached: false,
  49. zoomable: (gOptions.zoomable !== undefined)
  50. ? gOptions.zoomable
  51. : false,
  52. onthumbenter: (gOptions.onthumbenter !== undefined)
  53. ? gOptions.onthumbenter
  54. : null,
  55. onthumbmove: (gOptions.onthumbmove !== undefined)
  56. ? gOptions.onthumbmove
  57. : null,
  58. onthumbleave: (gOptions.onthumbleave !== undefined)
  59. ? gOptions.onthumbleave
  60. : null,
  61. onzoom: (gOptions.onzoom !== undefined)
  62. ? gOptions.onzoom
  63. : null
  64. },
  65. pos = {
  66. t: 0,
  67. l: 0,
  68. x: 0,
  69. y: 0
  70. },
  71. gId = 0,
  72. status = 0,
  73. curIdx = '',
  74. curLens = null,
  75. curLarge = null,
  76. gZoom = (gOptions.zoom !== undefined)
  77. ? gOptions.zoom
  78. : curData.zoom,
  79. gZoomMin = (gOptions.zoomMin !== undefined)
  80. ? gOptions.zoomMin
  81. : curData.zoomMin,
  82. gZoomMax = (gOptions.zoomMax !== undefined)
  83. ? gOptions.zoomMax
  84. : curData.zoomMax,
  85. gMode = gOptions.mode || curData.mode,
  86. data = {},
  87. inBounds = false,
  88. isOverThumb = 0,
  89. getElementsByClass = function (className) {
  90. var list = [],
  91. elements = null,
  92. len = 0,
  93. pattern = '',
  94. i = 0,
  95. j = 0;
  96. if (document.getElementsByClassName) {
  97. list = document.getElementsByClassName(className);
  98. } else {
  99. elements = document.getElementsByTagName('*');
  100. len = elements.length;
  101. pattern = new RegExp("(^|\\s)" + className + "(\\s|$)");
  102. for (i, j; i < len; i += 1) {
  103. if (pattern.test(elements[i].className)) {
  104. list[j] = elements[i];
  105. j += 1;
  106. }
  107. }
  108. }
  109. return list;
  110. },
  111. $ = function (selector) {
  112. var idx = '',
  113. type = selector.charAt(0),
  114. result = null;
  115. if (type === '#' || type === '.') {
  116. idx = selector.substr(1, selector.length);
  117. }
  118. if (idx !== '') {
  119. switch (type) {
  120. case '#':
  121. result = document.getElementById(idx);
  122. break;
  123. case '.':
  124. result = getElementsByClass(idx);
  125. break;
  126. }
  127. }
  128. return result;
  129. },
  130. createLens = function (thumb, idx) {
  131. var lens = document.createElement('div');
  132. lens.id = idx + '-lens';
  133. lens.className = 'magnifier-loader';
  134. thumb.parentNode.appendChild(lens);
  135. },
  136. updateLensOnZoom = function () {
  137. curLens.style.left = pos.l + 'px';
  138. curLens.style.top = pos.t + 'px';
  139. curLens.style.width = curData.lensW + 'px';
  140. curLens.style.height = curData.lensH + 'px';
  141. curLens.style.backgroundPosition = '-' + curData.lensBgX + 'px -' +
  142. curData.lensBgY + 'px';
  143. curLarge.style.left = '-' + curData.largeL + 'px';
  144. curLarge.style.top = '-' + curData.largeT + 'px';
  145. curLarge.style.width = curData.largeW + 'px';
  146. curLarge.style.height = curData.largeH + 'px';
  147. },
  148. updateLensOnLoad = function (idx, thumb, large, largeWrapper) {
  149. var lens = $('#' + idx + '-lens'),
  150. textWrapper = null;
  151. if (data[idx].status === 1) {
  152. textWrapper = document.createElement('div');
  153. textWrapper.className = 'magnifier-loader-text';
  154. lens.className = 'magnifier-loader hidden';
  155. textWrapper.appendChild(document.createTextNode('Loading...'));
  156. lens.appendChild(textWrapper);
  157. } else if (data[idx].status === 2) {
  158. lens.className = 'magnifier-lens hidden';
  159. lens.removeChild(lens.childNodes[0]);
  160. lens.style.background = 'url(' + thumb.src + ') no-repeat 0 0 scroll';
  161. large.id = idx + '-large';
  162. large.style.width = data[idx].largeW + 'px';
  163. large.style.height = data[idx].largeH + 'px';
  164. large.className = 'magnifier-large hidden';
  165. if (data[idx].mode === 'inside') {
  166. lens.appendChild(large);
  167. } else {
  168. largeWrapper.appendChild(large);
  169. }
  170. }
  171. lens.style.width = data[idx].lensW + 'px';
  172. lens.style.height = data[idx].lensH + 'px';
  173. },
  174. getMousePos = function () {
  175. var xPos = pos.x - curData.x,
  176. yPos = pos.y - curData.y,
  177. t = 0,
  178. l = 0;
  179. inBounds = (
  180. xPos < 0 ||
  181. yPos < 0 ||
  182. xPos > curData.w ||
  183. yPos > curData.h
  184. )
  185. ? false
  186. : true;
  187. l = xPos - (curData.lensW / 2);
  188. t = yPos - (curData.lensH / 2);
  189. if (curData.mode !== 'inside') {
  190. if (xPos < curData.lensW / 2) {
  191. l = 0;
  192. }
  193. if (yPos < curData.lensH / 2) {
  194. t = 0;
  195. }
  196. if (xPos - curData.w + (curData.lensW / 2) > 0) {
  197. l = curData.w - (curData.lensW + 2);
  198. }
  199. if (yPos - curData.h + (curData.lensH / 2) > 0) {
  200. t = curData.h - (curData.lensH + 2);
  201. }
  202. }
  203. pos.l = Math.round(l);
  204. pos.t = Math.round(t);
  205. curData.lensBgX = pos.l + 1;
  206. curData.lensBgY = pos.t + 1;
  207. if (curData.mode === 'inside') {
  208. curData.largeL = Math.round(xPos * (curData.zoom - (curData.lensW / curData.w)));
  209. curData.largeT = Math.round(yPos * (curData.zoom - (curData.lensH / curData.h)));
  210. } else {
  211. curData.largeL = Math.round(curData.lensBgX * curData.zoom * (curData.largeWrapperW / curData.w));
  212. curData.largeT = Math.round(curData.lensBgY * curData.zoom * (curData.largeWrapperH / curData.h));
  213. }
  214. },
  215. zoomInOut = function (e) {
  216. var delta = (e.wheelDelta > 0 || e.detail < 0) ? 0.1 : -0.1,
  217. handler = curData.onzoom,
  218. multiplier = 1,
  219. w = 0,
  220. h = 0;
  221. if (e.preventDefault) {
  222. e.preventDefault();
  223. }
  224. e.returnValue = false;
  225. curData.zoom = Math.round((curData.zoom + delta) * 10) / 10;
  226. if (curData.zoom >= curData.zoomMax) {
  227. curData.zoom = curData.zoomMax;
  228. } else if (curData.zoom >= curData.zoomMin) {
  229. curData.lensW = Math.round(curData.w / curData.zoom);
  230. curData.lensH = Math.round(curData.h / curData.zoom);
  231. if (curData.mode === 'inside') {
  232. w = curData.w;
  233. h = curData.h;
  234. } else {
  235. w = curData.largeWrapperW;
  236. h = curData.largeWrapperH;
  237. multiplier = curData.largeWrapperW / curData.w;
  238. }
  239. curData.largeW = Math.round(curData.zoom * w);
  240. curData.largeH = Math.round(curData.zoom * h);
  241. getMousePos();
  242. updateLensOnZoom();
  243. if (handler !== null) {
  244. handler({
  245. thumb: curThumb,
  246. lens: curLens,
  247. large: curLarge,
  248. x: pos.x,
  249. y: pos.y,
  250. zoom: Math.round(curData.zoom * multiplier * 10) / 10,
  251. w: curData.lensW,
  252. h: curData.lensH
  253. });
  254. }
  255. } else {
  256. curData.zoom = curData.zoomMin;
  257. }
  258. },
  259. onThumbEnter = function () {
  260. curData = data[curIdx];
  261. curLens = $('#' + curIdx + '-lens');
  262. if (curData.status === 2) {
  263. curLens.className = 'magnifier-lens';
  264. if (curData.zoomAttached === false) {
  265. if (curData.zoomable !== undefined && curData.zoomable === true) {
  266. evt.attach('mousewheel', curLens, zoomInOut);
  267. if (window.addEventListener) {
  268. curLens.addEventListener('DOMMouseScroll', function (e) {
  269. zoomInOut(e);
  270. });
  271. }
  272. }
  273. curData.zoomAttached = true;
  274. }
  275. curLarge = $('#' + curIdx + '-large');
  276. curLarge.className = 'magnifier-large';
  277. } else if (curData.status === 1) {
  278. curLens.className = 'magnifier-loader';
  279. }
  280. },
  281. onThumbLeave = function () {
  282. if (curData.status > 0) {
  283. var handler = curData.onthumbleave;
  284. if (handler !== null) {
  285. handler({
  286. thumb: curThumb,
  287. lens: curLens,
  288. large: curLarge,
  289. x: pos.x,
  290. y: pos.y
  291. });
  292. }
  293. if (curLens.className.indexOf('hidden') === -1) {
  294. curLens.className += ' hidden';
  295. curThumb.className = curData.thumbCssClass;
  296. if (curLarge !== null) {
  297. curLarge.className += ' hidden';
  298. }
  299. }
  300. }
  301. },
  302. move = function () {
  303. if (status !== curData.status) {
  304. onThumbEnter();
  305. }
  306. if (curData.status > 0) {
  307. curThumb.className = curData.thumbCssClass + ' opaque';
  308. if (curData.status === 1) {
  309. curLens.className = 'magnifier-loader';
  310. } else if (curData.status === 2) {
  311. curLens.className = 'magnifier-lens';
  312. curLarge.className = 'magnifier-large';
  313. curLarge.style.left = '-' + curData.largeL + 'px';
  314. curLarge.style.top = '-' + curData.largeT + 'px';
  315. }
  316. curLens.style.left = pos.l + 'px';
  317. curLens.style.top = pos.t + 'px';
  318. curLens.style.backgroundPosition = '-' +
  319. curData.lensBgX + 'px -' +
  320. curData.lensBgY + 'px';
  321. var handler = curData.onthumbmove;
  322. if (handler !== null) {
  323. handler({
  324. thumb: curThumb,
  325. lens: curLens,
  326. large: curLarge,
  327. x: pos.x,
  328. y: pos.y
  329. });
  330. }
  331. }
  332. status = curData.status;
  333. },
  334. setThumbData = function (thumb, thumbData) {
  335. var thumbBounds = thumb.getBoundingClientRect(),
  336. w = 0,
  337. h = 0;
  338. thumbData.x = thumbBounds.left;
  339. thumbData.y = thumbBounds.top;
  340. thumbData.w = Math.round(thumbBounds.right - thumbData.x);
  341. thumbData.h = Math.round(thumbBounds.bottom - thumbData.y);
  342. thumbData.lensW = Math.round(thumbData.w / thumbData.zoom);
  343. thumbData.lensH = Math.round(thumbData.h / thumbData.zoom);
  344. if (thumbData.mode === 'inside') {
  345. w = thumbData.w;
  346. h = thumbData.h;
  347. } else {
  348. w = thumbData.largeWrapperW;
  349. h = thumbData.largeWrapperH;
  350. }
  351. thumbData.largeW = Math.round(thumbData.zoom * w);
  352. thumbData.largeH = Math.round(thumbData.zoom * h);
  353. };
  354. this.attach = function (options) {
  355. if (options.thumb === undefined) {
  356. throw {
  357. name: 'Magnifier error',
  358. message: 'Please set thumbnail',
  359. toString: function () {return this.name + ": " + this.message; }
  360. };
  361. }
  362. var thumb = $(options.thumb),
  363. i = 0;
  364. if (thumb.length !== undefined) {
  365. for (i; i < thumb.length; i += 1) {
  366. options.thumb = thumb[i];
  367. this.set(options);
  368. }
  369. } else {
  370. options.thumb = thumb;
  371. this.set(options);
  372. }
  373. };
  374. this.setThumb = function (thumb) {
  375. curThumb = thumb;
  376. };
  377. this.set = function (options) {
  378. if (data[options.thumb.id] !== undefined) {
  379. curThumb = options.thumb;
  380. return false;
  381. }
  382. var thumbObj = new Image(),
  383. largeObj = new Image(),
  384. thumb = options.thumb,
  385. idx = thumb.id,
  386. zoomable = null,
  387. largeUrl = null,
  388. largeWrapper = (
  389. $('#' + options.largeWrapper) ||
  390. $('#' + thumb.getAttribute('data-large-img-wrapper')) ||
  391. $('#' + curData.largeWrapperId)
  392. ),
  393. zoom = options.zoom || thumb.getAttribute('data-zoom') || gZoom,
  394. zoomMin = options.zoomMin || thumb.getAttribute('data-zoom-min') || gZoomMin,
  395. zoomMax = options.zoomMax || thumb.getAttribute('data-zoom-max') || gZoomMax,
  396. mode = options.mode || thumb.getAttribute('data-mode') || gMode,
  397. onthumbenter = (options.onthumbenter !== undefined)
  398. ? options.onthumbenter
  399. : curData.onthumbenter,
  400. onthumbleave = (options.onthumbleave !== undefined)
  401. ? options.onthumbleave
  402. : curData.onthumbleave,
  403. onthumbmove = (options.onthumbmove !== undefined)
  404. ? options.onthumbmove
  405. : curData.onthumbmove,
  406. onzoom = (options.onzoom !== undefined)
  407. ? options.onzoom
  408. : curData.onzoom;
  409. if (options.large === undefined) {
  410. largeUrl = (options.thumb.getAttribute('data-large-img-url') !== null)
  411. ? options.thumb.getAttribute('data-large-img-url')
  412. : options.thumb.src;
  413. } else {
  414. largeUrl = options.large;
  415. }
  416. if (largeWrapper === null && mode !== 'inside') {
  417. throw {
  418. name: 'Magnifier error',
  419. message: 'Please specify large image wrapper DOM element',
  420. toString: function () {return this.name + ": " + this.message; }
  421. };
  422. }
  423. if (options.zoomable !== undefined) {
  424. zoomable = options.zoomable;
  425. } else if (thumb.getAttribute('data-zoomable') !== null) {
  426. zoomable = (thumb.getAttribute('data-zoomable') === 'true');
  427. } else if (curData.zoomable !== undefined) {
  428. zoomable = curData.zoomable;
  429. }
  430. if (thumb.id === '') {
  431. idx = thumb.id = 'magnifier-item-' + gId;
  432. gId += 1;
  433. }
  434. createLens(thumb, idx);
  435. data[idx] = {
  436. zoom: zoom,
  437. zoomMin: zoomMin,
  438. zoomMax: zoomMax,
  439. mode: mode,
  440. zoomable: zoomable,
  441. thumbCssClass: thumb.className,
  442. zoomAttached: false,
  443. status: 0,
  444. largeUrl: largeUrl,
  445. largeWrapperId: mode === 'outside' ? largeWrapper.id : null,
  446. largeWrapperW: mode === 'outside' ? largeWrapper.offsetWidth : null,
  447. largeWrapperH: mode === 'outside' ? largeWrapper.offsetHeight : null,
  448. onzoom: onzoom,
  449. onthumbenter: onthumbenter,
  450. onthumbleave: onthumbleave,
  451. onthumbmove: onthumbmove
  452. };
  453. evt.attach('mouseover', thumb, function (e, src) {
  454. if (curData.status !== 0) {
  455. onThumbLeave();
  456. }
  457. curIdx = src.id;
  458. curThumb = src;
  459. onThumbEnter(src);
  460. setThumbData(curThumb, curData);
  461. pos.x = e.clientX;
  462. pos.y = e.clientY;
  463. getMousePos();
  464. move();
  465. var handler = curData.onthumbenter;
  466. if (handler !== null) {
  467. handler({
  468. thumb: curThumb,
  469. lens: curLens,
  470. large: curLarge,
  471. x: pos.x,
  472. y: pos.y
  473. });
  474. }
  475. }, false);
  476. evt.attach('mousemove', thumb, function (e, src) {
  477. isOverThumb = 1;
  478. });
  479. evt.attach('load', thumbObj, function () {
  480. data[idx].status = 1;
  481. setThumbData(thumb, data[idx]);
  482. updateLensOnLoad(idx);
  483. evt.attach('load', largeObj, function () {
  484. data[idx].status = 2;
  485. updateLensOnLoad(idx, thumb, largeObj, largeWrapper);
  486. });
  487. largeObj.src = data[idx].largeUrl;
  488. });
  489. thumbObj.src = thumb.src;
  490. };
  491. evt.attach('mousemove', document, function (e) {
  492. pos.x = e.clientX;
  493. pos.y = e.clientY;
  494. getMousePos();
  495. if (inBounds === true) {
  496. move();
  497. } else {
  498. if (isOverThumb !== 0) {
  499. onThumbLeave();
  500. }
  501. isOverThumb = 0;
  502. }
  503. }, false);
  504. evt.attach('scroll', window, function () {
  505. if (curThumb !== null) {
  506. setThumbData(curThumb, curData);
  507. }
  508. });
  509. };