UNPKG

jquery.magnify

Version:

A jQuery plugin to view images just like in Windows

1,207 lines (1,053 loc) 38.4 kB
/** * Private static constants */ var $W = $(window), $D = $(document), CLICK_EVENT = 'click', RESIZE_EVENT = 'resize', KEYDOWN_EVENT = 'keydown', WHEEL_EVENT = 'wheel mousewheel DOMMouseScroll', TOUCH_START_EVENT = supportTouch() ? 'touchstart' : 'mousedown', TOUCH_MOVE_EVENT = supportTouch() ? 'touchmove' : 'mousemove', TOUCH_END_EVENT = supportTouch() ? 'touchend' : 'mouseup', NS = 'magnify', EVENT_NS = '.' + NS, // Plugin default options DEFAULTS = { // Enable modal to drag draggable: true, // Enable modal to resize resizable: true, // Enable image to move movable: true, // Enable keyboard navigation keyboard: true, // Shows the title title: true, // Min width of modal modalWidth: 320, // Min height of modal modalHeight: 320, // Enable the page content fixed fixedContent: true, // Disable the modal size fixed fixedModalSize: false, // Disable the image viewer maximized on init initMaximized: false, // Threshold of modal to browser window gapThreshold: 0.02, // Threshold of image ratio ratioThreshold: 0.1, // Min ratio of image when zoom out minRatio: 0.05, // Max ratio of image when zoom in maxRatio: 16, // Toolbar options in header headerToolbar: ['maximize', 'close'], // Toolbar options in footer footerToolbar: [ 'zoomIn', 'zoomOut', 'prev', 'fullscreen', 'next', 'actualSize', 'rotateRight' ], // Customize button icon icons: { minimize: '<svg viewBox="0 0 1024 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M1024 749.714v109.714c0 50.286-41.143 91.429-91.429 91.429\ h-841.143c-50.286 0-91.429-41.143-91.429-91.429v-109.714c0-50.286 41.143-91.429 91.429\ -91.429h841.143c50.286 0 91.429 41.143 91.429 91.429z"></path>\ </svg>', maximize: '<svg viewBox="0 0 1024 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M146.286 804.571h731.429v-438.857h-731.429v438.857z\ M1024 164.571v694.857c0 50.286-41.143 91.429-91.429 91.429h-841.143c-50.286 0\ -91.429-41.143-91.429-91.429v-694.857c0-50.286 41.143-91.429 91.429-91.429\ h841.143c50.286 0 91.429 41.143 91.429 91.429z"></path>\ </svg>', close: '<svg viewBox="0 0 804.5714285714286 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M741.714 755.429c0 14.286-5.714 28.571-16 38.857\ l-77.714 77.714c-10.286 10.286-24.571 16-38.857 16s-28.571-5.714-38.857-16l-168-168\ -168 168c-10.286 10.286-24.571 16-38.857 16s-28.571-5.714-38.857-16l-77.714-77.714\ c-10.286-10.286-16-24.571-16-38.857s5.714-28.571 16-38.857l168-168-168-168c-10.286-10.286\ -16-24.571-16-38.857s5.714-28.571 16-38.857l77.714-77.714c10.286-10.286 24.571-16 38.857\ -16s28.571 5.714 38.857 16l168 168 168-168c10.286-10.286 24.571-16 38.857-16\ s28.571 5.714 38.857 16l77.714 77.714c10.286 10.286 16 24.571 16 38.857s-5.714 28.571\ -16 38.857l-168 168 168 168c10.286 10.286 16 24.571 16 38.857z"></path>\ </svg>', zoomIn: '<svg viewBox="0 0 950.8571428571428 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M585.143 457.143v36.571c0 9.714-8.571 18.286-18.286 18.286\ h-128v128c0 9.714-8.571 18.286-18.286 18.286h-36.571c-9.714 0-18.286-8.571-18.286-18.286\ v-128h-128c-9.714 0-18.286-8.571-18.286-18.286v-36.571c0-9.714 8.571-18.286 18.286-18.286\ h128v-128c0-9.714 8.571-18.286 18.286-18.286h36.571c9.714 0 18.286 8.571 18.286 18.286\ v128h128c9.714 0 18.286 8.571 18.286 18.286zM658.286 475.429c0-141.143-114.857-256-256\ -256s-256 114.857-256 256 114.857 256 256 256 256-114.857 256-256zM950.857 950.857\ c0 40.571-32.571 73.143-73.143 73.143-19.429 0-38.286-8-51.429-21.714l-196-195.429\ c-66.857 46.286-146.857 70.857-228 70.857-222.286 0-402.286-180-402.286-402.286s180\ -402.286 402.286-402.286 402.286 180 402.286 402.286c0 81.143-24.571 161.143-70.857 228\ l196 196c13.143 13.143 21.143 32 21.143 51.429z"></path>\ </svg>', zoomOut: '<svg viewBox="0 0 950.8571428571428 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M585.143 457.143v36.571c0 9.714-8.571 18.286-18.286 18.286\ h-329.143c-9.714 0-18.286-8.571-18.286-18.286v-36.571c0-9.714 8.571-18.286 18.286-18.286\ h329.143c9.714 0 18.286 8.571 18.286 18.286zM658.286 475.429c0-141.143-114.857-256-256\ -256s-256 114.857-256 256 114.857 256 256 256 256-114.857 256-256zM950.857 950.857\ c0 40.571-32.571 73.143-73.143 73.143-19.429 0-38.286-8-51.429-21.714l-196-195.429\ c-66.857 46.286-146.857 70.857-228 70.857-222.286 0-402.286-180-402.286-402.286s180\ -402.286 402.286-402.286 402.286 180 402.286 402.286c0 81.143-24.571 161.143-70.857 228\ l196 196c13.143 13.143 21.143 32 21.143 51.429z"></path>\ </svg>', prev: '<svg viewBox="0 0 914.2857142857142 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M877.714 512v73.143c0 38.857-25.714 73.143-66.857 73.143\ h-402.286l167.429 168c13.714 13.143 21.714 32 21.714 51.429s-8 38.286-21.714 51.429\ l-42.857 43.429c-13.143 13.143-32 21.143-51.429 21.143s-38.286-8-52-21.143l-372-372.571\ c-13.143-13.143-21.143-32-21.143-51.429s8-38.286 21.143-52l372-371.429c13.714\ -13.714 32.571-21.714 52-21.714s37.714 8 51.429 21.714l42.857 42.286\ c13.714 13.714 21.714 32.571 21.714 52s-8 38.286-21.714 52l-167.429 167.429h402.286\ c41.143 0 66.857 34.286 66.857 73.143z"></path>\ </svg>', next: '<svg viewBox="0 0 841.1428571428571 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M841.143 548.571c0 19.429-7.429 38.286-21.143 52l-372 372\ c-13.714 13.143-32.571 21.143-52 21.143s-37.714-8-51.429-21.143l-42.857-42.857c-13.714\ -13.714-21.714-32.571-21.714-52s8-38.286 21.714-52l167.429-167.429h-402.286c-41.143 0\ -66.857-34.286-66.857-73.143v-73.143c0-38.857 25.714-73.143 66.857-73.143h402.286\ l-167.429-168c-13.714-13.143-21.714-32-21.714-51.429s8-38.286 21.714-51.429l42.857\ -42.857c13.714-13.714 32-21.714 51.429-21.714s38.286 8 52 21.714l372 372\ c13.714 13.143 21.143 32 21.143 51.429z"></path>\ </svg>', fullscreen: '<svg viewBox="0 0 1097.142857142857 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M365.714 329.143c0 60.571-49.143 109.714-109.714 109.714\ s-109.714-49.143-109.714-109.714 49.143-109.714 109.714\ -109.714 109.714 49.143 109.714 109.714zM950.857 548.571v256h-804.571v-109.714l182.857\ -182.857 91.429 91.429 292.571-292.571zM1005.714 146.286h-914.286c-9.714 0-18.286 8.571\ -18.286 18.286v694.857c0 9.714 8.571 18.286 18.286 18.286h914.286c9.714 0 18.286\ -8.571 18.286-18.286v-694.857c0-9.714-8.571-18.286-18.286-18.286zM1097.143 164.571\ v694.857c0 50.286-41.143 91.429-91.429 91.429h-914.286c-50.286 0-91.429-41.143-91.429\ -91.429v-694.857c0-50.286 41.143-91.429 91.429-91.429h914.286\ c50.286 0 91.429 41.143 91.429 91.429z"></path>\ </svg>', actualSize: '<svg viewBox="0 0 877.7142857142857 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M733.143 309.143l-202.857 202.857 202.857 202.857 82.286\ -82.286c10.286-10.857 26.286-13.714 40-8 13.143 5.714 22.286 18.857 22.286 33.714v256\ c0 20-16.571 36.571-36.571 36.571h-256c-14.857 0-28-9.143-33.714-22.857-5.714-13.143\ -2.857-29.143 8-39.429l82.286-82.286-202.857-202.857-202.857 202.857 82.286 82.286\ c10.857 10.286 13.714 26.286 8 39.429-5.714 13.714-18.857 22.857-33.714 22.857h-256\ c-20 0-36.571-16.571-36.571-36.571v-256c0-14.857 9.143-28 22.857-33.714 13.143\ -5.714 29.143-2.857 39.429 8l82.286 82.286 202.857-202.857-202.857-202.857-82.286 82.286\ c-6.857 6.857-16 10.857-25.714 10.857-4.571 0-9.714-1.143-13.714-2.857-13.714-5.714\ -22.857-18.857-22.857-33.714v-256c0-20 16.571-36.571 36.571-36.571h256\ c14.857 0 28 9.143 33.714 22.857 5.714 13.143 2.857 29.143-8 39.429\ l-82.286 82.286 202.857 202.857 202.857-202.857-82.286-82.286c-10.857-10.286-13.714\ -26.286-8-39.429 5.714-13.714 18.857-22.857 33.714-22.857h256\ c20 0 36.571 16.571 36.571 36.571v256c0 14.857-9.143 28-22.286 33.714-4.571 1.714\ -9.714 2.857-14.286 2.857-9.714 0-18.857-4-25.714-10.857z"></path>\ </svg>', rotateLeft: '<svg viewBox="0 0 877.7142857142857 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M877.714 512c0 241.714-197.143 438.857-438.857 438.857\ -130.857 0-254.286-57.714-337.714-158.286-5.714-7.429-5.143-18.286 1.143-24.571l78.286\ -78.857c4-3.429 9.143-5.143 14.286\ -5.143 5.143 0.571 10.286 2.857 13.143 6.857 56 72.571 140 113.714 230.857 113.714 161.143 0 292.571\ -131.429 292.571-292.571s-131.429-292.571-292.571-292.571c-74.857 0-145.714 28.571\ -198.857 78.286l78.286 78.857c10.857 10.286 13.714 26.286 8 39.429-5.714 13.714\ -18.857 22.857-33.714 22.857h-256c-20 0-36.571-16.571-36.571-36.571v-256\ c0-14.857 9.143-28 22.857-33.714 13.143-5.714 29.143-2.857 39.429 8l74.286 73.714\ c80.571-76 189.714-121.143 302.286-121.143 241.714 0 438.857 197.143 438.857 438.857z"></path>\ </svg>', rotateRight: '<svg viewBox="0 0 877.7142857142857 1024" class="svg-inline-icon">\ <path fill="currentColor" d="M877.714 146.286v256c0 20-16.571 36.571-36.571 36.571h-256\ c-14.857 0-28-9.143-33.714-22.857-5.714-13.143-2.857-29.143 8-39.429l78.857-78.857\ c-53.714-49.714-124.571-78.286-199.429-78.286-161.143 0-292.571 131.429-292.571 292.571\ s131.429 292.571 292.571 292.571c90.857 0 174.857-41.143 230.857-113.714 2.857-4 8\ -6.286 13.143-6.857 5.143 0 10.286 1.714 14.286 5.143l78.286 78.857\ c6.857 6.286 6.857 17.143 1.143 24.571-83.429 100.571-206.857 158.286-337.714 158.286\ -241.714 0-438.857-197.143-438.857-438.857s197.143-438.857 438.857-438.857\ c112.571 0 221.714 45.143 302.286 121.143l74.286-73.714c10.286-10.857 26.286-13.714 40\ -8 13.143 5.714 22.286 18.857 22.286 33.714z"></path>\ </svg>' }, // Customize language of button title i18n: { minimize: 'minimize', maximize: 'maximize', close: 'close', zoomIn: 'zoom-in(+)', zoomOut: 'zoom-out(-)', prev: 'prev(←)', next: 'next(→)', fullscreen: 'fullscreen', actualSize: 'actual-size(Ctrl+Alt+0)', rotateLeft: 'rotate-left(Ctrl+,)', rotateRight: 'rotate-right(Ctrl+.)' }, // Enable multiple instances multiInstances: true, // Init trigger event initEvent: 'click', // Enable animation initAnimation: true, // Disable modal position fixed when change images fixedModalPos: false, // Modal z-index zIndex: 1090, // Selector of drag handler dragHandle: false, // Callback events callbacks: { beforeOpen: $.noop, opened: $.noop, beforeClose: $.noop, closed: $.noop, beforeChange: $.noop, changed: $.noop }, // Load the image progressively progressiveLoading: true, // Custom Buttons customButtons: {} }, PUBLIC_VARS = { // Image moving flag isMoving: false, // Modal resizing flag isResizing: false, // Modal z-index setting zIndex: DEFAULTS.zIndex }; // jQuery element of calling plugin var jqEl = null; /** * Magnify Class */ var Magnify = function (el, options) { var self = this; this.options = $.extend(true, {}, DEFAULTS, options); if (options && $.isArray(options.footerToolbar)) { this.options.footerToolbar = options.footerToolbar; } if (options && $.isArray(options.headerToolbar)) { this.options.headerToolbar = options.headerToolbar; } // Store element of clicked this.$el = $(el); // As we have multiple instances, // so every instance has following variables. // modal open flag this.isOpened = false; // modal maximize flag this.isMaximized = false; // image rotate 90*(2n+1) flag this.isRotated = false; // image rotate angle this.rotateAngle = 0; // if modal do resize this.isDoResize = false; // Store image data in every instance this.imageData = {}; // Store modal data in every instance this.modalData = { width: null, height: null, left: null, top: null }; this.init(el, self.options); }; /** * Mangify Prototype */ Magnify.prototype = { init: function (el, opts) { // Get image src var imgSrc = getImageSrc(el); // Get image group this.groupName = null; var currentGroupName = $(el).attr('data-group'), groupList = $D.find('[data-group="' + currentGroupName + '"]'); if (currentGroupName !== undefined) { this.groupName = currentGroupName; this.getImageGroup(groupList, imgSrc); } else { this.getImageGroup(jqEl.not('[data-group]'), imgSrc); } this.open(); this.loadImage(imgSrc); // Draggable & Movable & Resizable if (opts.draggable) { this.draggable(this.$magnify, this.dragHandle, '.magnify-button'); } if (opts.movable) { this.movable(this.$stage, isIE8() ? '.magnify-image' : this.$image); } if (opts.resizable) { this.resizable( this.$magnify, this.$stage, isIE8() ? '.magnify-image' : this.$image, opts.modalWidth, opts.modalHeight ); } }, _createBtns: function (toolbar) { var self = this; var btns = [ 'minimize', 'maximize', 'close', 'zoomIn', 'zoomOut', 'prev', 'next', 'fullscreen', 'actualSize', 'rotateLeft', 'rotateRight' ]; var btnsHTML = ''; $.each(toolbar, function (index, item) { if ($.inArray(item, btns) >= 0) { btnsHTML += '<button class="magnify-button magnify-button-' + item + '" title="' + self.options.i18n[item] + '">' + self.options.icons[item] + '</button>'; } else if (self.options.customButtons[item]) { btnsHTML += '<button class="magnify-button magnify-button-' + item + '" title="' + (self.options.customButtons[item].title || '') + '">' + self.options.customButtons[item].text + '</button>'; } }); return btnsHTML; }, _createTitle: function () { return this.options.title ? '<div class="magnify-title"></div>' : ''; }, _createTemplate: function () { // Magnify base HTML var magnifyHTML = '<div class="magnify-modal" tabindex="0">\ <div class="magnify-header">\ <div class="magnify-toolbar">' + this._createBtns(this.options.headerToolbar) + '\ </div>' + this._createTitle() + '\ </div>\ <div class="magnify-stage">\ <img class="magnify-image" src="" alt="" />\ </div>\ <div class="magnify-footer">\ <div class="magnify-toolbar">' + this._createBtns(this.options.footerToolbar) + '\ </div>\ </div>\ </div>'; return magnifyHTML; }, build: function () { // Create magnify HTML string var magnifyHTML = this._createTemplate(); // Make magnify HTML string to jQuery element var $magnify = $(magnifyHTML); // Get all magnify element this.$magnify = $magnify; this.$stage = $magnify.find('.magnify-stage'); this.$title = $magnify.find('.magnify-title'); this.$image = $magnify.find('.magnify-image'); this.$close = $magnify.find('.magnify-button-close'); this.$maximize = $magnify.find('.magnify-button-maximize'); this.$minimize = $magnify.find('.magnify-button-minimize'); this.$zoomIn = $magnify.find('.magnify-button-zoomIn'); this.$zoomOut = $magnify.find('.magnify-button-zoomOut'); this.$actualSize = $magnify.find('.magnify-button-actualSize'); this.$fullscreen = $magnify.find('.magnify-button-fullscreen'); this.$rotateLeft = $magnify.find('.magnify-button-rotateLeft'); this.$rotateRight = $magnify.find('.magnify-button-rotateRight'); this.$prev = $magnify.find('.magnify-button-prev'); this.$next = $magnify.find('.magnify-button-next'); // Add class before image loaded this.$stage.addClass('stage-ready'); this.$image.addClass('image-ready'); // Reset modal z-index with multiple instances this.$magnify.css('z-index', PUBLIC_VARS['zIndex']); // Set handle element of draggable if ( !this.options.dragHandle || this.options.dragHandle === '.magnify-modal' ) { this.dragHandle = this.$magnify; } else { this.dragHandle = this.$magnify.find(this.options.dragHandle); } // Add Magnify to DOM $('body').append(this.$magnify); this._addEvents(); this._addCustomButtonEvents(); }, open: function () { this._triggerHook('beforeOpen', this); if (!this.options.multiInstances) { $('.magnify-modal').eq(0).remove(); } // Fixed modal position bug if (!$('.magnify-modal').length && this.options.fixedContent) { $('html').css({ overflow: 'hidden' }); if (hasScrollbar()) { var scrollbarWidth = getScrollbarWidth(); if (scrollbarWidth) { $('html').css({ 'padding-right': scrollbarWidth }); } } } this.build(); this.setModalPos(this.$magnify); this.$magnify.focus(); this._triggerHook('opened', this); }, close: function (el) { this._triggerHook('beforeClose', this); // Remove instance this.$magnify.remove(); this.isOpened = false; this.isMaximized = false; this.isRotated = false; this.rotateAngle = 0; if (!$('.magnify-modal').length) { // Fixed modal position bug if (this.options.fixedContent) { $('html').css({ overflow: '', 'padding-right': '' }); } // Reset zIndex after close if (this.options.multiInstances) { PUBLIC_VARS['zIndex'] = this.options.zIndex; } // off resize events $W.off(RESIZE_EVENT + EVENT_NS); } this._triggerHook('closed', this); }, setModalPos: function (modal) { var winWidth = $W.width(), winHeight = $W.height(), scrollLeft = $D.scrollLeft(), scrollTop = $D.scrollTop(); var modalWidth = this.options.modalWidth, modalHeight = this.options.modalHeight; // Set modal maximized when init if (this.options.initMaximized) { modal.addClass('magnify-maximize'); modal.css({ width: '100%', height: '100%', left: 0, top: 0 }); this.isOpened = true; this.isMaximized = true; } else { // Make the modal in windows center modal.css({ width: modalWidth, height: modalHeight, left: (winWidth - modalWidth) / 2 + scrollLeft + 'px', top: (winHeight - modalHeight) / 2 + scrollTop + 'px' }); } }, setModalSize: function (img) { var self = this, winWidth = $W.width(), winHeight = $W.height(), scrollLeft = $D.scrollLeft(), scrollTop = $D.scrollTop(); // Stage css value var stageCSS = { left: this.$stage.css('left'), right: this.$stage.css('right'), top: this.$stage.css('top'), bottom: this.$stage.css('bottom'), borderLeft: this.$stage.css('border-left-width'), borderRight: this.$stage.css('border-right-width'), borderTop: this.$stage.css('border-top-width'), borderBottom: this.$stage.css('border-bottom-width') }; // Modal size should calc with stage css value var modalWidth = img.width + parseFloat(stageCSS.left) + parseFloat(stageCSS.right) + parseFloat(stageCSS.borderLeft) + parseFloat(stageCSS.borderRight); var modalHeight = img.height + parseFloat(stageCSS.top) + parseFloat(stageCSS.bottom) + parseFloat(stageCSS.borderTop) + parseFloat(stageCSS.borderBottom); var gapThreshold = (this.options.gapThreshold > 0 ? this.options.gapThreshold : 0) + 1; // Modal scale to window var scale = Math.min( winWidth / (modalWidth * gapThreshold), winHeight / (modalHeight * gapThreshold), 1 ); var minWidth = Math.max(modalWidth * scale, this.options.modalWidth); var minHeight = Math.max(modalHeight * scale, this.options.modalHeight); minWidth = this.options.fixedModalSize ? this.options.modalWidth : Math.round(minWidth); minHeight = this.options.fixedModalSize ? this.options.modalHeight : Math.round(minHeight); var modalCSSObj = { width: minWidth + 'px', height: minHeight + 'px', left: (winWidth - minWidth) / 2 + scrollLeft + 'px', top: (winHeight - minHeight) / 2 + scrollTop + 'px' }; // Add modal init animation if (this.options.initAnimation) { this.$magnify.animate(modalCSSObj, function () { self.setImageSize(img); }); } else { this.$magnify.css(modalCSSObj); this.setImageSize(img); } this.isOpened = true; }, getImageScaleToStage: function (stageWidth, stageHeight) { var scale = 1; if (!this.isRotated) { scale = Math.min( stageWidth / this.img.width, stageHeight / this.img.height, 1 ); } else { scale = Math.min( stageWidth / this.img.height, stageHeight / this.img.width, 1 ); } return scale; }, setImageSize: function (img) { var $image = isIE8() ? this.$stage.find('.magnify-image') : this.$image; var stageData = { w: this.$stage.width(), h: this.$stage.height() }; var scale = this.getImageScaleToStage(stageData.w, stageData.h); $image.css({ width: Math.ceil(img.width * scale) + 'px', height: Math.ceil(img.height * scale) + 'px', left: (stageData.w - Math.ceil(img.width * scale)) / 2 + 'px', top: (stageData.h - Math.ceil(img.height * scale)) / 2 + 'px' }); if (isIE8()) { $image.find('group').css({ width: Math.floor(img.width * scale) + 'px', height: Math.floor(img.height * scale) + 'px' }); } // Store image initial data $.extend(this.imageData, { initWidth: img.width * scale, initHeight: img.height * scale, initLeft: (stageData.w - img.width * scale) / 2, initTop: (stageData.h - img.height * scale) / 2, width: img.width * scale, height: img.height * scale, left: (stageData.w - img.width * scale) / 2, top: (stageData.h - img.height * scale) / 2 }); // Set grab cursor setGrabCursor( { w: $image.width(), h: $image.height() }, { w: this.$stage.width(), h: this.$stage.height() }, this.$stage, this.isRotated ); // Just execute before image loaded if (!this.imageLoaded) { // loader end this.$magnify.find('.magnify-loader').remove(); // Remove class must when image setting end this.$stage.removeClass('stage-ready'); this.$image.removeClass('image-ready'); // Add image init animation if (this.options.initAnimation && !this.options.progressiveLoading) { $image.fadeIn(); } this.imageLoaded = true; } }, loadImage: function (imgSrc, fn, err) { var self = this; // Reset image this.$image.removeAttr('style').attr('src', ''); this.isRotated = false; this.rotateAngle = 0; this.imageLoaded = false; // Loader start this.$magnify.append('<div class="magnify-loader"></div>'); // Add class before image loaded this.$stage.addClass('stage-ready'); this.$image.addClass('image-ready'); if (this.options.initAnimation && !this.options.progressiveLoading) { this.$image.hide(); } if (isIE8()) { this.$stage.html( '<img class="magnify-image" id="magnify-image" src="' + imgSrc + '" alt="" />' ); } else { this.$image.attr('src', imgSrc); } preloadImage( imgSrc, function (img) { // Store HTMLImageElement self.img = img; // Store original data self.imageData = { originalWidth: img.width, originalHeight: img.height }; if (self.isMaximized || (self.isOpened && self.options.fixedModalPos)) { self.setImageSize(img); } else { self.setModalSize(img); } // Callback of image loaded successfully if (fn) { fn.call(); } }, function () { // Loader end self.$magnify.find('.magnify-loader').remove(); // Callback of image loading failed if (err) { err.call(); } } ); if (this.options.title) { this.setImageTitle(imgSrc); } }, getImageGroup: function (list, imgSrc) { var self = this; self.groupData = []; $(list).each(function (index, item) { var _imgSrc = getImageSrc(this); self.groupData.push({ src: _imgSrc, caption: $(this).attr('data-caption') }); // Get image index if (imgSrc === _imgSrc) { self.groupIndex = index; } }); }, setImageTitle: function (url) { var title = this.groupData[this.groupIndex].caption || getImageNameFromUrl(url); this.$title.html(title); }, jump: function (step) { this._triggerHook('beforeChange', [this, this.groupIndex]); this.groupIndex = this.groupIndex + step; this.jumpTo(this.groupIndex); }, jumpTo: function (index) { var self = this; index = index % this.groupData.length; if (index >= 0) { index = index % this.groupData.length; } else if (index < 0) { index = (this.groupData.length + index) % this.groupData.length; } this.groupIndex = index; this.loadImage( this.groupData[index].src, function () { self._triggerHook('changed', [self, index]); }, function () { self._triggerHook('changed', [self, index]); } ); }, wheel: function (e) { e.preventDefault(); var delta = 1; if (e.originalEvent.deltaY) { delta = e.originalEvent.deltaY > 0 ? 1 : -1; } else if (e.originalEvent.wheelDelta) { delta = -e.originalEvent.wheelDelta / 120; } else if (e.originalEvent.detail) { delta = e.originalEvent.detail > 0 ? 1 : -1; } // Ratio threshold var ratio = -delta * this.options.ratioThreshold; // Mouse point position relative to stage var pointer = { x: e.originalEvent.clientX - this.$stage.offset().left + $D.scrollLeft(), y: e.originalEvent.clientY - this.$stage.offset().top + $D.scrollTop() }; this.zoom(ratio, pointer, e); }, zoom: function (ratio, origin, e) { this.$image = isIE8() ? this.$stage.find('.magnify-image') : this.$image; // Zoom out ratio & Zoom in ratio ratio = ratio < 0 ? 1 / (1 - ratio) : 1 + ratio; // Image ratio ratio = (this.$image.width() / this.imageData.originalWidth) * ratio; // Fixed digital error // if (ratio > 0.95 && ratio < 1.05) { // ratio = 1; // } if (ratio > this.options.maxRatio || ratio < this.options.minRatio) { return; } this.zoomTo(ratio, origin, e); }, zoomTo: function (ratio, origin, e) { var $image = isIE8() ? this.$stage.find('.magnify-image') : this.$image; var $stage = this.$stage; var imgData = { w: this.imageData.width, h: this.imageData.height, x: this.imageData.left, y: this.imageData.top }; // Image stage position // We will use it to calc the relative position of image var stageData = { w: $stage.width(), h: $stage.height(), x: $stage.offset().left, y: $stage.offset().top }; var newWidth = this.imageData.originalWidth * ratio; var newHeight = this.imageData.originalHeight * ratio; // Think about it for a while var newLeft = origin.x - ((origin.x - imgData.x) / imgData.w) * newWidth; var newTop = origin.y - ((origin.y - imgData.y) / imgData.h) * newHeight; // δ is the difference between image new width and new height var δ = !this.isRotated ? 0 : (newWidth - newHeight) / 2; var imgNewWidth = !this.isRotated ? newWidth : newHeight; var imgNewHeight = !this.isRotated ? newHeight : newWidth; var offsetX = stageData.w - newWidth, offsetY = stageData.h - newHeight; // Zoom out & Zoom in condition // It's important and it takes me a lot of time to get it // The conditions with image rotate 90 degree drive me crazy alomst! if (imgNewHeight <= stageData.h) { newTop = (stageData.h - newHeight) / 2; } else { newTop = newTop > δ ? δ : newTop > offsetY - δ ? newTop : offsetY - δ; } if (imgNewWidth <= stageData.w) { newLeft = (stageData.w - newWidth) / 2; } else { newLeft = newLeft > -δ ? -δ : newLeft > offsetX + δ ? newLeft : offsetX + δ; } // If the image scale get to the critical point if (Math.abs(this.imageData.initWidth - newWidth) < this.imageData.initWidth * 0.05) { this.setImageSize(this.img); } else { $image.css({ width: Math.round(newWidth) + 'px', height: Math.round(newHeight) + 'px', left: Math.round(newLeft) + 'px', top: Math.round(newTop) + 'px' }); if (isIE8()) { $image.find('group').css({ width: Math.ceil(newWidth) + 'px', height: Math.ceil(newHeight) + 'px' }); } // Set grab cursor setGrabCursor( { w: Math.round(imgNewWidth), h: Math.round(imgNewHeight) }, { w: stageData.w, h: stageData.h }, this.$stage ); } // Update image initial data $.extend(this.imageData, { width: newWidth, height: newHeight, left: newLeft, top: newTop }); }, rotate: function (angle) { this.rotateAngle = this.rotateAngle + angle; if ((this.rotateAngle / 90) % 2 === 0) { this.isRotated = false; } else { this.isRotated = true; } this.rotateTo(this.rotateAngle); }, rotateTo: function (angle) { var $image = isIE8() ? this.$stage.find('.magnify-image') : this.$image; // Depend on jQueryRotate.js $image.rotate({ angle: angle }); this.setImageSize({ width: this.imageData.originalWidth, height: this.imageData.originalHeight }); // Remove grab cursor when rotate this.$stage.removeClass('is-grab'); }, resize: function () { var self = this; var resizeHandler = throttle(function () { if (self.isOpened) { if (self.isMaximized) { self.setImageSize({ width: self.imageData.originalWidth, height: self.imageData.originalHeight }); } else { self.setModalSize({ width: self.imageData.originalWidth, height: self.imageData.originalHeight }); } } }, 500); return resizeHandler; }, maximize: function () { this.$magnify.focus(); if (!this.isMaximized) { // Store modal data before maximize this.modalData = { width: this.$magnify.width(), height: this.$magnify.height(), left: this.$magnify.offset().left, top: this.$magnify.offset().top }; this.$magnify.addClass('magnify-maximize'); this.$magnify.css({ width: '100%', height: '100%', left: 0, top: 0 }); this.isMaximized = true; } else { this.$magnify.removeClass('magnify-maximize'); var initModalLeft = ($W.width() - this.options.modalWidth) / 2 + $D.scrollLeft(); var initModalTop = ($W.height() - this.options.modalHeight) / 2 + $D.scrollTop(); this.$magnify.css({ width: this.modalData.width ? this.modalData.width : this.options.modalWidth, height: this.modalData.height ? this.modalData.height : this.options.modalHeight, left: this.modalData.left ? this.modalData.left : initModalLeft, top: this.modalData.top ? this.modalData.top : initModalTop }); this.isMaximized = false; } this.setImageSize({ width: this.imageData.originalWidth, height: this.imageData.originalHeight }); }, fullscreen: function () { this.$magnify.focus(); requestFullscreen(this.$magnify[0]); }, _keydown: function (e) { var self = this; if (!this.options.keyboard) { return false; } var keyCode = e.keyCode || e.which || e.charCode, ctrlKey = e.ctrlKey || e.metaKey, altKey = e.altKey || e.metaKey; switch (keyCode) { // ← case 37: self.jump(-1); break; // → case 39: self.jump(1); break; // + case 187: self.zoom( self.options.ratioThreshold * 3, { x: self.$stage.width() / 2, y: self.$stage.height() / 2 }, e ); break; // - case 189: self.zoom( -self.options.ratioThreshold * 3, { x: self.$stage.width() / 2, y: self.$stage.height() / 2 }, e ); break; // + Firefox case 61: self.zoom( self.options.ratioThreshold * 3, { x: self.$stage.width() / 2, y: self.$stage.height() / 2 }, e ); break; // - Firefox case 173: self.zoom( -self.options.ratioThreshold * 3, { x: self.$stage.width() / 2, y: self.$stage.height() / 2 }, e ); break; // Ctrl + Alt + 0 case 48: if (ctrlKey && altKey) { self.zoomTo( 1, { x: self.$stage.width() / 2, y: self.$stage.height() / 2 }, e ); } break; // Ctrl + , case 188: if (ctrlKey) { self.rotate(-90); } break; // Ctrl + . case 190: if (ctrlKey) { self.rotate(90); } break; // Q case 81: this.close(); break; default: } }, _addEvents: function () { var self = this; this.$close.off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function (e) { self.close(); }); this.$stage.off(WHEEL_EVENT + EVENT_NS).on(WHEEL_EVENT + EVENT_NS, function (e) { self.wheel(e); }); this.$zoomIn.off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function (e) { self.zoom( self.options.ratioThreshold * 3, { x: self.$stage.width() / 2, y: self.$stage.height() / 2 }, e ); }); this.$zoomOut.off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function (e) { self.zoom( -self.options.ratioThreshold * 3, { x: self.$stage.width() / 2, y: self.$stage.height() / 2 }, e ); }); this.$actualSize.off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function (e) { self.zoomTo( 1, { x: self.$stage.width() / 2, y: self.$stage.height() / 2 }, e ); }); this.$prev.off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function () { self.jump(-1); }); this.$fullscreen.off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function () { self.fullscreen(); }); this.$next.off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function () { self.jump(1); }); this.$rotateLeft.off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function () { self.rotate(-90); }); this.$rotateRight.off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function () { self.rotate(90); }); this.$maximize.off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function () { self.maximize(); }); this.$magnify.off(KEYDOWN_EVENT + EVENT_NS).on(KEYDOWN_EVENT + EVENT_NS, function (e) { self._keydown(e); }); $W.on(RESIZE_EVENT + EVENT_NS, self.resize()); }, _addCustomButtonEvents: function () { var self = this; for (var btnKey in self.options.customButtons) { this.$magnify.find('.magnify-button-' + btnKey) .off(CLICK_EVENT + EVENT_NS).on(CLICK_EVENT + EVENT_NS, function (e) { self.options.customButtons[btnKey].click.apply(self, [self, e]); }); } }, _triggerHook: function (e, data) { if (this.options.callbacks[e]) { this.options.callbacks[e].apply(this, $.isArray(data) ? data : [data]); } } }; /** * jQuery plugin */ $.fn.magnify = function (options) { jqEl = $(this); // Convert a numeric string into a number for (var key in options) { if (typeof options[key] === 'string' && !isNaN(options[key])) { options[key] = parseFloat(options[key]); } } // Get init event, 'click' or 'dblclick' var opts = $.extend(true, {}, DEFAULTS, options); // We should get zIndex of options before plugin's init. PUBLIC_VARS['zIndex'] = opts.zIndex; if (typeof options === 'string') { // $(this).data('magnify')[options](); } else { if (opts.initEvent === 'dblclick') { jqEl.off('click' + EVENT_NS).on('click' + EVENT_NS, function (e) { e.preventDefault(); // This will stop triggering data-api event e.stopPropagation(); }); } jqEl.off(opts.initEvent + EVENT_NS).on(opts.initEvent + EVENT_NS, function (e) { e.preventDefault(); // This will stop triggering data-api event e.stopPropagation(); $(this).data('magnify', new Magnify(this, options)); }); } return jqEl; }; /** * MAGNIFY DATA-API */ $D.on(CLICK_EVENT + EVENT_NS, '[data-magnify]', function (e) { jqEl = $('[data-magnify]'); e.preventDefault(); $(this).data('magnify', new Magnify(this, DEFAULTS)); });