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 <[email protected]> 22*/ 23var Magnifier = function (evt, options) { 24 "use strict"; 25 26 var gOptions = options || {}, 27 curThumb = null, 28 curData = { 29 x: 0, 30 y: 0, 31 w: 0, 32 h: 0, 33 lensW: 0, 34 lensH: 0, 35 lensBgX: 0, 36 lensBgY: 0, 37 largeW: 0, 38 largeH: 0, 39 largeL: 0, 40 largeT: 0, 41 zoom: 2, 42 zoomMin: 1.1, 43 zoomMax: 5, 44 mode: 'outside', 45 largeWrapperId: (gOptions.largeWrapper !== undefined) 46 ? (gOptions.largeWrapper.id || null) 47 : null, 48 status: 0, 49 zoomAttached: false, 50 zoomable: (gOptions.zoomable !== undefined) 51 ? gOptions.zoomable 52 : false, 53 onthumbenter: (gOptions.onthumbenter !== undefined) 54 ? gOptions.onthumbenter 55 : null, 56 onthumbmove: (gOptions.onthumbmove !== undefined) 57 ? gOptions.onthumbmove 58 : null, 59 onthumbleave: (gOptions.onthumbleave !== undefined) 60 ? gOptions.onthumbleave 61 : null, 62 onzoom: (gOptions.onzoom !== undefined) 63 ? gOptions.onzoom 64 : null 65 }, 66 pos = { 67 t: 0, 68 l: 0, 69 x: 0, 70 y: 0 71 }, 72 gId = 0, 73 status = 0, 74 curIdx = '', 75 curLens = null, 76 curLarge = null, 77 gZoom = (gOptions.zoom !== undefined) 78 ? gOptions.zoom 79 : curData.zoom, 80 gZoomMin = (gOptions.zoomMin !== undefined) 81 ? gOptions.zoomMin 82 : curData.zoomMin, 83 gZoomMax = (gOptions.zoomMax !== undefined) 84 ? gOptions.zoomMax 85 : curData.zoomMax, 86 gMode = gOptions.mode || curData.mode, 87 data = {}, 88 inBounds = false, 89 isOverThumb = 0, 90 getElementsByClass = function (className) { 91 var list = [], 92 elements = null, 93 len = 0, 94 pattern = '', 95 i = 0, 96 j = 0; 97 98 if (document.getElementsByClassName) { 99 list = document.getElementsByClassName(className); 100 } else { 101 elements = document.getElementsByTagName('*'); 102 len = elements.length; 103 pattern = new RegExp("(^|\\s)" + className + "(\\s|$)"); 104 105 for (i, j; i < len; i += 1) { 106 if (pattern.test(elements[i].className)) { 107 list[j] = elements[i]; 108 j += 1; 109 } 110 } 111 } 112 113 return list; 114 }, 115 $ = function (selector) { 116 var idx = '', 117 type = selector.charAt(0), 118 result = null; 119 120 if (type === '#' || type === '.') { 121 idx = selector.substr(1, selector.length); 122 } 123 124 if (idx !== '') { 125 switch (type) { 126 case '#': 127 result = document.getElementById(idx); 128 break; 129 case '.': 130 result = getElementsByClass(idx); 131 break; 132 } 133 } 134 135 return result; 136 }, 137 createLens = function (thumb, idx) { 138 var lens = document.createElement('div'); 139 140 lens.id = idx + '-lens'; 141 lens.className = 'magnifier-loader'; 142 143 thumb.parentNode.appendChild(lens); 144 }, 145 updateLensOnZoom = function () { 146 curLens.style.left = pos.l + 'px'; 147 curLens.style.top = pos.t + 'px'; 148 curLens.style.width = curData.lensW + 'px'; 149 curLens.style.height = curData.lensH + 'px'; 150 curLens.style.backgroundPosition = '-' + curData.lensBgX + 'px -' + 151 curData.lensBgY + 'px'; 152 153 curLarge.style.left = '-' + curData.largeL + 'px'; 154 curLarge.style.top = '-' + curData.largeT + 'px'; 155 curLarge.style.width = curData.largeW + 'px'; 156 curLarge.style.height = curData.largeH + 'px'; 157 }, 158 updateLensOnLoad = function (idx, thumb, large, largeWrapper) { 159 var lens = $('#' + idx + '-lens'), 160 textWrapper = null; 161 162 if (data[idx].status === 1) { 163 textWrapper = document.createElement('div'); 164 textWrapper.className = 'magnifier-loader-text'; 165 lens.className = 'magnifier-loader hidden'; 166 167 textWrapper.appendChild(document.createTextNode('Loading...')); 168 lens.appendChild(textWrapper); 169 } else if (data[idx].status === 2) { 170 lens.className = 'magnifier-lens hidden'; 171 lens.removeChild(lens.childNodes[0]); 172 lens.style.background = 'url(' + thumb.src + ') no-repeat 0 0 scroll'; 173 174 large.id = idx + '-large'; 175 large.style.width = data[idx].largeW + 'px'; 176 large.style.height = data[idx].largeH + 'px'; 177 large.className = 'magnifier-large hidden'; 178 179 if (data[idx].mode === 'inside') { 180 lens.appendChild(large); 181 } else { 182 largeWrapper.appendChild(large); 183 } 184 } 185 186 lens.style.width = data[idx].lensW + 'px'; 187 lens.style.height = data[idx].lensH + 'px'; 188 }, 189 getMousePos = function () { 190 var xPos = pos.x - curData.x, 191 yPos = pos.y - curData.y, 192 t = 0, 193 l = 0; 194 195 inBounds = ( 196 xPos < 0 || 197 yPos < 0 || 198 xPos > curData.w || 199 yPos > curData.h 200 ) 201 ? false 202 : true; 203 204 l = xPos - (curData.lensW / 2); 205 t = yPos - (curData.lensH / 2); 206
207 if (curData.mode !== 'inside') { 208 if (xPos < curData.lensW / 2) { 209 l = 0; 210 } 211 212 if (yPos < curData.lensH / 2) { 213 t = 0; 214 } 215 216 if (xPos - curData.w + (curData.lensW / 2) > 0) { 217 l = curData.w - (curData.lensW + 2); 218 } 219 220 if (yPos - curData.h + (curData.lensH / 2) > 0) { 221 t = curData.h - (curData.lensH + 2); 222 } 223 } 224 225 pos.l = Math.round(l); 226 pos.t = Math.round(t); 227 228 curData.lensBgX = pos.l + 1; 229 curData.lensBgY = pos.t + 1; 230 231 if (curData.mode === 'inside') { 232 curData.largeL = Math.round(xPos * (curData.zoom - (curData.lensW / curData.w))); 233 curData.largeT = Math.round(yPos * (curData.zoom - (curData.lensH / curData.h))); 234 } else { 235 curData.largeL = Math.round(curData.lensBgX * curData.zoom * (curData.largeWrapperW / curData.w)); 236 curData.largeT = Math.round(curData.lensBgY * curData.zoom * (curData.largeWrapperH / curData.h)); 237 } 238 }, 239 zoomInOut = function (e) { 240 var delta = (e.wheelDelta > 0 || e.detail < 0) ? 0.1 : -0.1, 241 handler = curData.onzoom, 242 multiplier = 1, 243 w = 0, 244 h = 0; 245 246 if (e.preventDefault) { 247 e.preventDefault(); 248 } 249 250 e.returnValue = false; 251 252 curData.zoom = Math.round((curData.zoom + delta) * 10) / 10; 253 254 if (curData.zoom >= curData.zoomMax) { 255 curData.zoom = curData.zoomMax; 256 } else if (curData.zoom >= curData.zoomMin) { 257 curData.lensW = Math.round(curData.w / curData.zoom); 258 curData.lensH = Math.round(curData.h / curData.zoom); 259 260 if (curData.mode === 'inside') { 261 w = curData.w; 262 h = curData.h; 263 } else { 264 w = curData.largeWrapperW; 265 h = curData.largeWrapperH; 266 multiplier = curData.largeWrapperW / curData.w; 267 } 268 269 curData.largeW = Math.round(curData.zoom * w); 270 curData.largeH = Math.round(curData.zoom * h); 271 272 getMousePos(); 273 updateLensOnZoom(); 274 275 if (handler !== null) { 276 handler({ 277 thumb: curThumb, 278 lens: curLens, 279 large: curLarge, 280 x: pos.x, 281 y: pos.y, 282 zoom: Math.round(curData.zoom * multiplier * 10) / 10, 283 w: curData.lensW, 284 h: curData.lensH 285 }); 286 } 287 } else { 288 curData.zoom = curData.zoomMin; 289 } 290 }, 291 onThumbEnter = function () { 292 curData = data[curIdx]; 293 curLens = $('#' + curIdx + '-lens'); 294 295 if (curData.status === 2) { 296 curLens.className = 'magnifier-lens'; 297 298 if (curData.zoomAttached === false) { 299 if (curData.zoomable !== undefined && curData.zoomable === true) { 300 evt.attach('mousewheel', curLens, zoomInOut); 301 302 if (window.addEventListener) { 303 curLens.addEventListener('DOMMouseScroll', function (e) { 304 zoomInOut(e); 305 }); 306 } 307 } 308 309 curData.zoomAttached = true; 310 } 311 312 curLarge = $('#' + curIdx + '-large'); 313 curLarge.className = 'magnifier-large'; 314 } else if (curData.status === 1) { 315 curLens.className = 'magnifier-loader'; 316 } 317 }, 318 onThumbLeave = function () { 319 if (curData.status > 0) { 320 var handler = curData.onthumbleave; 321 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 if (curLens.className.indexOf('hidden') === -1) { 333 curLens.className += ' hidden'; 334 curThumb.className = curData.thumbCssClass; 335 336 if (curLarge !== null) { 337 curLarge.className += ' hidden'; 338 } 339 } 340 } 341 }, 342 move = function () { 343 if (status !== curData.status) { 344 onThumbEnter(); 345 } 346 347 if (curData.status > 0) { 348 curThumb.className = curData.thumbCssClass + ' opaque'; 349 350 if (curData.status === 1) { 351 curLens.className = 'magnifier-loader'; 352 } else if (curData.status === 2) { 353 curLens.className = 'magnifier-lens'; 354 curLarge.className = 'magnifier-large'; 355 curLarge.style.left = '-' + curData.largeL + 'px'; 356 curLarge.style.top = '-' + curData.largeT + 'px'; 357 } 358 359 curLens.style.left = pos.l + 'px'; 360 curLens.style.top = pos.t + 'px'; 361 curLens.style.backgroundPosition = '-' + 362 curData.lensBgX + 'px -' + 363 curData.lensBgY + 'px'; 364 365 var handler = curData.onthumbmove; 366 367 if (handler !== null) { 368 handler({ 369 thumb: curThumb, 370 lens: curLens, 371 large: curLarge, 372 x: pos.x, 373 y: pos.y 374 }); 375 } 376 } 377 378 status = curData.status; 379 }, 380 setThumbData = function (thumb, thumbData) { 381 var thumbBounds = thumb.getBoundingClientRect(), 382 w = 0, 383 h = 0; 384 385 thumbData.x = thumbBounds.left; 386 thumbData.y = thumbBounds.top; 387 thumbData.w = Math.round(thumbBounds.right - thumbData.x); 388 thumbData.h = Math.round(thumbBounds.bottom - thumbData.y); 389 390 thumbData.lensW = Math.round(thumbData.w / thumbData.zoom); 391 thumbData.lensH = Math.round(thumbData.h / thumbData.zoom); 392 393 if (thumbData.mode === 'inside') { 394 w = thumbData.w; 395 h = thumbData.h; 396 } else { 397 w = thumbData.largeWrapperW; 398 h = thumbData.largeWrapperH; 399 } 400 401 thumbData.largeW = Math.round(thumbData.zoom * w); 402 thumbData.largeH = Math.round(thumbData.zoom * h); 403 }; 404 405 this.attach = function (options) { 406 if (options.thumb === undefined) { 407 throw { 408 name: 'Magnifier error', 409 message: 'Please set thumbnail', 410 toString: function () {return this.name + ": " + this.message; } 411 }; 412 } 413 414 var thumb = $(options.thumb), 415 i = 0; 416 417 if (thumb.length !== undefined) { 418 for (i; i < thumb.length; i += 1) { 419 options.thumb = thumb[i]; 420 this.set(options); 421 } 422 } else { 423 options.thumb = thumb; 424 this.set(options); 425 } 426 }; 427 428 this.setThumb = function (thumb) { 429 curThumb = thumb; 430 }; 431 432 this.set = function (options) { 433 if (data[options.thumb.id] !== undefined) { 434 curThumb = options.thumb; 435 return false;
436 } 437 438 var thumbObj = new Image(), 439 largeObj = new Image(), 440 thumb = options.thumb, 441 idx = thumb.id, 442 zoomable = null, 443 largeUrl = null, 444 largeWrapper = ( 445 $('#' + options.largeWrapper) || 446 $('#' + thumb.getAttribute('data-large-img-wrapper')) || 447 $('#' + curData.largeWrapperId) 448 ), 449 zoom = options.zoom || thumb.getAttribute('data-zoom') || gZoom, 450 zoomMin = options.zoomMin || thumb.getAttribute('data-zoom-min') || gZoomMin, 451 zoomMax = options.zoomMax || thumb.getAttribute('data-zoom-max') || gZoomMax, 452 mode = options.mode || thumb.getAttribute('data-mode') || gMode, 453 onthumbenter = (options.onthumbenter !== undefined) 454 ? options.onthumbenter 455 : curData.onthumbenter, 456 onthumbleave = (options.onthumbleave !== undefined) 457 ? options.onthumbleave 458 : curData.onthumbleave, 459 onthumbmove = (options.onthumbmove !== undefined) 460 ? options.onthumbmove 461 : curData.onthumbmove, 462 onzoom = (options.onzoom !== undefined) 463 ? options.onzoom 464 : curData.onzoom; 465 466 if (options.large === undefined) { 467 largeUrl = (options.thumb.getAttribute('data-large-img-url') !== null) 468 ? options.thumb.getAttribute('data-large-img-url') 469 : options.thumb.src; 470 } else { 471 largeUrl = options.large; 472 } 473 474 if (largeWrapper === null && mode !== 'inside') { 475 throw { 476 name: 'Magnifier error', 477 message: 'Please specify large image wrapper DOM element', 478 toString: function () {return this.name + ": " + this.message; } 479 }; 480 } 481 482 if (options.zoomable !== undefined) { 483 zoomable = options.zoomable; 484 } else if (thumb.getAttribute('data-zoomable') !== null) { 485 zoomable = (thumb.getAttribute('data-zoomable') === 'true'); 486 } else if (curData.zoomable !== undefined) { 487 zoomable = curData.zoomable; 488 } 489 490 if (thumb.id === '') { 491 idx = thumb.id = 'magnifier-item-' + gId; 492 gId += 1; 493 } 494 495 createLens(thumb, idx); 496 497 data[idx] = { 498 zoom: zoom, 499 zoomMin: zoomMin, 500 zoomMax: zoomMax, 501 mode: mode, 502 zoomable: zoomable, 503 thumbCssClass: thumb.className, 504 zoomAttached: false, 505 status: 0, 506 largeUrl: largeUrl, 507 largeWrapperId: mode === 'outside' ? largeWrapper.id : null, 508 largeWrapperW: mode === 'outside' ? largeWrapper.offsetWidth : null, 509 largeWrapperH: mode === 'outside' ? largeWrapper.offsetHeight : null, 510 onzoom: onzoom, 511 onthumbenter: onthumbenter, 512 onthumbleave: onthumbleave, 513 onthumbmove: onthumbmove 514 }; 515 516 evt.attach('mouseover', thumb, function (e, src) { 517 if (curData.status !== 0) { 518 onThumbLeave(); 519 } 520 521 curIdx = src.id; 522 curThumb = src; 523 524 onThumbEnter(src); 525 526 setThumbData(curThumb, curData); 527 528 pos.x = e.clientX; 529 pos.y = e.clientY; 530 531 getMousePos(); 532 move(); 533 534 var handler = curData.onthumbenter; 535 536 if (handler !== null) { 537 handler({ 538 thumb: curThumb, 539 lens: curLens, 540 large: curLarge, 541 x: pos.x, 542 y: pos.y 543 }); 544 } 545 }, false); 546 547 evt.attach('mousemove', thumb, function (e, src) { 548 isOverThumb = 1; 549 }); 550 551 evt.attach('load', thumbObj, function () { 552 data[idx].status = 1; 553 554 setThumbData(thumb, data[idx]); 555 updateLensOnLoad(idx); 556 557 evt.attach('load', largeObj, function () { 558 data[idx].status = 2; 559 updateLensOnLoad(idx, thumb, largeObj, largeWrapper); 560 }); 561 562 largeObj.src = data[idx].largeUrl; 563 }); 564 565 thumbObj.src = thumb.src; 566 }; 567 568 evt.attach('mousemove', document, function (e) { 569 pos.x = e.clientX; 570 pos.y = e.clientY; 571 572 getMousePos(); 573 574 if (inBounds === true) { 575 move(); 576 } else { 577 if (isOverThumb !== 0) { 578 onThumbLeave(); 579 } 580 581 isOverThumb = 0; 582 } 583 }, false); 584 585 evt.attach('scroll', window, function () { 586 if (curThumb !== null) { 587 setThumbData(curThumb, curData); 588 } 589 }); 590}; 591 592 593/** 594* Unifies event handling across browsers 595* 596* - Allows registering and unregistering of event handlers 597* - Injects event object and involved DOM element to listener 598* 599* @author Mark Rolich <[email protected]> 600*/ 601var Event = function () { 602 "use strict"; 603 this.attach = function (evtName, element, listener, capture) { 604 var evt = '', 605 useCapture = (capture === undefined) ? true : capture, 606 handler = null; 607 608 if (window.addEventListener === undefined) { 609 evt = 'on' + evtName; 610 handler = function (evt, listener) { 611 element.attachEvent(evt, listener); 612 return listener; 613 }; 614 } else { 615 evt = evtName; 616 handler = function (evt, listener, useCapture) { 617 element.addEventListener(evt, listener, useCapture); 618 return listener; 619 }; 620 } 621 622 return handler.apply(element, [evt, function (ev) { 623 var e = ev || event, 624 src = e.srcElement || e.target; 625
626 listener(e, src); 627 }, useCapture]); 628 }; 629 630 this.detach = function (evtName, element, listener, capture) { 631 var evt = '', 632 useCapture = (capture === undefined) ? true : capture; 633 634 if (window.removeEventListener === undefined) { 635 evt = 'on' + evtName; 636 element.detachEvent(evt, listener); 637 } else { 638 evt = evtName; 639 element.removeEventListener(evt, listener, useCapture); 640 } 641 }; 642 643 this.stop = function (evt) { 644 evt.cancelBubble = true; 645 646 if (evt.stopPropagation) { 647 evt.stopPropagation(); 648 } 649 }; 650 651 this.prevent = function (evt) { 652 if (evt.preventDefault) { 653 evt.preventDefault(); 654 } else { 655 evt.returnValue = false; 656 } 657 }; 658}; 659 660// Create object 661var evt = new Event(), 662m = new Magnifier(evt);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.