/*! * kwicks: sexy sliding panels for jquery - v2.2.1 * http://devsmash.com/projects/kwicks * * copyright 2013 jeremy martin (jmar777) * contributors: duke speer (duke3d), guillermo guerrero (gguerrero) * released under the mit license * http://www.opensource.org/licenses/mit-license.php */ (function($) { /** * api methods for the plugin */ var methods = { init: function(opts) { var defaults = { // general options: maxsize: -1, minsize: -1, spacing: 5, duration: 500, isvertical: false, easing: undefined, autoresize: true, behavior: null, // menu behavior options: delaymousein: 0, delaymouseout: 0, selectonclick: true, deselectonclick: false, // slideshow behavior options: interval: 2500, interactive: true }; var o = $.extend(defaults, opts); // validate and normalize options if (o.minsize !== -1 && o.maxsize !== -1) throw new error('kwicks options minsize and maxsize may not both be set'); if (o.behavior && o.behavior !== 'menu' && o.behavior !== 'slideshow') throw new error('unrecognized kwicks behavior specified: ' + o.behavior); $.each(['minsize', 'maxsize', 'spacing'], function(i, prop) { var val = o[prop]; switch (typeof val) { case 'number': o[prop + 'units'] = 'px'; break; case 'string': if (val.slice(-1) === '%') { o[prop + 'units'] = '%'; o[prop] = +val.slice(0, -1) / 100; } else if (val.slice(-2) === 'px') { o[prop + 'units'] = 'px'; o[prop] = +val.slice(0, -2); } else { throw new error('invalid value for kwicks option ' + prop + ': ' + val); } break; default: throw new error('invalid value for kwicks option ' + prop + ': ' + val); } }); return this.each(function() { $(this).data('kwicks', new kwick(this, o)); }); }, expand: function(index, opts) { if (typeof index === 'object') { opts = index; index = undefined; } var delay = opts && opts.delay || 0; return this.each(function() { var $this = $(this), kwick = $this.data('kwicks'); // assume this is the container if (kwick) { index = typeof index === 'number' ? index : -1; } // otherwise, assume we have a panel else if (kwick = $this.parent().data('kwicks')) { index = $this.index(); } else { return; } var expand = function() { // bail out if the panel is already expanded if (index === kwick.expandedindex) return; var $panels = kwick.$panels, expanded = $panels[index] || null; kwick.$container.trigger('expand.kwicks', { index: index, expanded: expanded, collapsed: $panels.not(expanded).get(), oldindex: kwick.expandedindex, oldexpanded: kwick.getexpandedpanel(), isanimated: kwick.isanimated }); }; var timeoutid = kwick.$container.data('kwicks-timeout-id'); if (timeoutid) { kwick.$container.removedata('kwicks-timeout-id'); cleartimeout(timeoutid); } if (delay > 0) { kwick.$container.data('kwicks-timeout-id', settimeout(expand, delay)); } else { expand(); } }); }, expanded: function() { var kwick = this.first().data('kwicks'); if (!kwick) return; return kwick.expandedindex; }, select: function(index) { return this.each(function() { var $this = $(this), kwick = $this.data('kwicks'); // assume this is the container if (kwick) { index = typeof index === 'number' ? index : -1; } // otherwise, assume we have a panel else if (kwick = $this.parent().data('kwicks')) { index = $this.index(); } else { return; } // don't trigger event if its already selected if (index !== kwick.selectedindex) { var $panels = kwick.$panels, selected = $panels[index] || null; kwick.$container.trigger('select.kwicks', { index: index, selected: selected, unselected: $panels.not(selected).get(), oldindex: kwick.selectedindex, oldselected: kwick.getselectedpanel() }); } // call expand kwick.$container.kwicks('expand', index); }); }, selected: function() { var kwick = this.first().data('kwicks'); if (!kwick) return; return kwick.selectedindex; }, resize: function() { return this.each(function() { var $this = $(this), kwick = $this.data('kwicks'); if (!kwick) return; kwick.resize(); }); }, destroy: function() { return this.each(function() { var $this = $(this), kwick = $this.data('kwicks'); if (!kwick) return; kwick.destroy(); }); } }; /** * expose the actual plugin */ $.fn.kwicks = function(opts) { if (methods[opts]) { return methods[opts].apply(this, array.prototype.slice.call(arguments, 1)); } else if (typeof opts === 'object' || !opts) { return methods.init.apply(this, arguments); } else { throw new error('unrecognized kwicks method: ' + opts); } }; /** * special event for triggering default behavior on 'expand.kwicks' events */ $.event.special.expand = { _default: function(e, data) { if (e.namespace !== 'kwicks') return; var kwick = $(e.target).data('kwicks'); if (kwick) kwick.expand(data.index); } }; /** * special event for triggering default behavior on 'select.kwicks' events */ $.event.special.select = { _default: function(e, data) { if (e.namespace !== 'kwicks') return; var kwick = $(e.target).data('kwicks'); if (kwick) kwick.select(data.index); } }; /** * instantiates a new kwick instance using the provided container and options. */ var kwick = function kwick(container, opts) { var self = this; this.opts = opts; // an array of callbacks to invoke if 'destroy' is invoked this.ondestroyhandlers = []; // references to our dom elements var orientation = opts.isvertical ? 'vertical' : 'horizontal'; this.$container = $(container); this.$panels = this.$container.children(); // semi-smart add/remove around container classes so that we don't bork // the styling if/when destroy is called var containerclasses = ['kwicks', 'kwicks-' + orientation]; $.each(containerclasses, function(classname) { if (self.$container.hasclass(classname)) return; self.$container.addclass(classname); self.ondestroy(function() { self.$container.removeclass(classname); }); }); // zero-based, -1 for "none" this.selectedindex = this.$panels.filter('.kwicks-selected').index(); this.expandedindex = this.selectedindex; // each instance has a primary and a secondary dimension (primary is the animated dimension) this.primarydimension = opts.isvertical ? 'height' : 'width'; this.secondarydimension = opts.isvertical ? 'width' : 'height'; // initialize panel sizes this.calculatepanelsizes(); // likewise, we have primary and secondary alignments (all panels but the last use primary, // which uses the secondary alignment). this is to allow the first and last panels to have // fixed offsets. this reduces jittering, which is much more noticeable on the last item. this.primaryalignment = opts.isvertical ? 'top' : 'left'; this.secondaryalignment = opts.isvertical ? 'bottom' : 'right'; // object for creating a "master" animation loop for all panel animations this.$timer = $({ progress: 0 }); // keeps track of whether or not an animation is in progress this.isanimated = false; // the current offsets for each panel this.offsets = this.getoffsetsforexpanded(); this.updatepanelstyles(); this.initbehavior(); this.initwindowresizehandler(); // somewhat of a blind stab at handling rare/sporadic failures to initialize styles. // https://github.com/jmar777/kwicks/issues/31 settimeout(function() { self.updatepanelstyles(); }, 100); }; /** * calculates size, minsize, maxsize, and spacing based on the current size of the container and * the user-provided options. the results will be stored on this.panelsize, this.panelminsize, * this.panelmaxsize, and this.panelspacing. this should be run on initialization and whenever * the container's primary dimension may have changed in size. */ kwick.prototype.calculatepanelsizes = function() { var opts = this.opts, containersize = this.getcontainersize(true); // calculate spacing first if (opts.spacingunits === '%') { this.panelspacing = containersize * opts.spacing; } else { this.panelspacing = opts.spacing; } var numpanels = this.$panels.length, sumspacing = this.panelspacing * (numpanels - 1), sumpanelsize = containersize - sumspacing; this.panelsize = sumpanelsize / numpanels; if (opts.minsize === -1) { if (opts.maxsize === -1) { // if neither minsize or maxsize or set, then we try to pick a sensible default if (numpanels < 5) { this.panelmaxsize = containersize / 3 * 2; } else { this.panelmaxsize = containersize / 3; } } else if (opts.maxsizeunits === '%') { this.panelmaxsize = sumpanelsize * opts.maxsize; } else { this.panelmaxsize = opts.maxsize; } // at this point we know that this.panelmaxsize is set this.panelminsize = (sumpanelsize - this.panelmaxsize) / (numpanels - 1); } else if (opts.maxsize === -1) { // at this point we know that opts.minsize is set if (opts.minsizeunits === '%') { this.panelminsize = sumpanelsize * opts.minsize; } else { this.panelminsize = opts.minsize; } // at this point we know that this.panelminsize is set this.panelmaxsize = sumpanelsize - (this.panelminsize * (numpanels - 1)); } }; /** * returns the calculated panel offsets based on the currently expanded panel. */ kwick.prototype.getoffsetsforexpanded = function() { // todo: cache the offset values var expandedindex = this.expandedindex, numpanels = this.$panels.length, spacing = this.panelspacing, size = this.panelsize, minsize = this.panelminsize, maxsize = this.panelmaxsize; //first panel is always offset by 0 var offsets = [0]; for (var i = 1; i < numpanels; i++) { // no panel is expanded if (expandedindex === -1) { offsets[i] = i * (size + spacing); } // this panel is before or is the expanded panel else if (i <= expandedindex) { offsets[i] = i * (minsize + spacing); } // this panel is after the expanded panel else { offsets[i] = maxsize + (minsize * (i - 1)) + (i * spacing); } } return offsets; }; /** * sets the style attribute on the specified element using the provided value. this probably * doesn't belong on kwick.prototype, but here it is... */ kwick.prototype.setstyle = (function() { if ($.support.style) { return function(el, style) { el.setattribute('style', style); }; } else { return function (el, style) { el.style.csstext = style; }; } })(); /** * updates the offset and size styling of each panel based on the current values in * `this.offsets`. also does some special handling to convert panels to absolute positioning * the first time this is invoked. */ kwick.prototype.updatepanelstyles = function() { var offsets = this.offsets, $panels = this.$panels, pdim = this.primarydimension, palign = this.primaryalignment, salign = this.secondaryalignment, spacing = this.panelspacing, containersize = this.getcontainersize(); // the kwicks-processed class ensures that panels are absolutely positioned, but on our // first pass we need to set offsets, width|length, and positioning atomically to prevent // mid-update repaints var styleprefix = !!this._stylesinited ? '' : 'position:absolute;', offset, size, prevoffset, style; // loop through remaining panels for (var i = $panels.length; i--;) { prevoffset = offset; // todo: maybe we do one last pass at the end and round offsets, rather than on every // update offset = math.round(offsets[i]); if (i === $panels.length - 1) { size = containersize - offset; style = salign + ':0;' + pdim + ':' + size + 'px;'; } else { size = prevoffset - offset - spacing; style = palign + ':' + offset + 'px;' + pdim + ':' + size + 'px;'; } this.setstyle($panels[i], styleprefix + style); } if (!this._stylesinited) { this.$container.addclass('kwicks-processed'); this._stylesinited = true; } }; /** * assuming for a moment that out-of-the-box behaviors aren't a horrible idea, this method * encapsulates the initialization logic thereof. */ kwick.prototype.initbehavior = function() { if (!this.opts.behavior) return; switch (this.opts.behavior) { case 'menu': this.initmenubehavior(); break; case 'slideshow': this.initslideshowbehavior(); break; default: throw new error('unrecognized behavior option: ' + this.opts.behavior); } }; /** * initializes the menu behavior. */ kwick.prototype.initmenubehavior = function() { var self = this, opts = self.opts; this.addeventhandler(this.$container, 'mouseleave', function() { self.$container.kwicks('expand', -1, { delay: opts.delaymouseout }); }); this.addeventhandler(this.$panels, 'mouseenter', function() { $(this).kwicks('expand', { delay: opts.delaymousein }); }); if (!opts.selectonclick && !opts.deselectonclick) return; this.addeventhandler(this.$panels, 'click', function() { var $this = $(this), isselected = $this.hasclass('kwicks-selected'); if (isselected && opts.deselectonclick) { $this.parent().kwicks('select', -1); } else if (!isselected && opts.selectonclick) { $this.kwicks('select'); } }); }; /** * initializes the slideshow behavior. */ kwick.prototype.initslideshowbehavior = function() { var self = this, numslides = this.$panels.length, curslide = 0, // flag to handle weird corner cases running = false, intervalid; var start = function() { if (running) return; intervalid = setinterval(function() { self.$container.kwicks('expand', ++curslide % numslides); }, self.opts.interval); running = true; }; var pause = function() { clearinterval(intervalid); running = false; }; start(); this.ondestroy(pause); if (!this.opts.interactive) return; this.addeventhandler(this.$container, 'mouseenter', pause); this.addeventhandler(this.$container, 'mouseleave', start); this.addeventhandler(this.$panels, 'mouseenter', function() { curslide = $(this).kwicks('expand').index(); }); }; /** * sets up a throttled window resize handler that triggers resize logic for the panels * todo: hideous code, needs refactor for the eye bleeds */ kwick.prototype.initwindowresizehandler = function() { if (!this.opts.autoresize) return; var self = this, prevtime = 0, execscheduled = false, $window = $(window); var onresize = function(e) { // if there's no event, then this is a scheduled from our settimeout if (!e) { execscheduled = false; } // if we've already run in the last 20ms, then delay execution var now = +new date(); if (now - prevtime < 20) { // if we already scheduled a run, don't do it again if (execscheduled) return; settimeout(onresize, 20 - (now - prevtime)); execscheduled = true; return; } // throttle rate is satisfied, go ahead and run prevtime = now; self.resize(); }; this.addeventhandler($window, 'resize', onresize); }; /** * returns the size in pixels of the container's primary dimension. this value is cached as it * is used repeatedly during animation loops, but the cache can be cleared by passing `true`. * todo: benchmark to see if this caching business is even at all necessary. */ kwick.prototype.getcontainersize = function(clearcache) { var containersize = this._containersize; if (clearcache || !containersize) { containersize = this._containersize = this.$container[this.primarydimension](); } return containersize; }; /** * gets a reference to the currently expanded panel (if there is one) */ kwick.prototype.getexpandedpanel = function() { return this.$panels[this.expandedindex] || null; }; /** * gets a reference to the currently collapsed panels */ kwick.prototype.getcollapsedpanels = function() { if (this.expandedindex === -1) return []; return this.$panels.not(this.getexpandedpanel()).get(); }; /** * gets a reference to the currently selected panel (if there is one) */ kwick.prototype.getselectedpanel = function() { return this.$panels[this.selectedindex] || null; }; /** * gets a reference to the currently unselected panels */ kwick.prototype.getunselectedpanels = function() { return this.$panels.not(this.getselectedpanel()).get(); }; /** * registers a handler to be invoked if/when 'destroy' is invoked */ kwick.prototype.ondestroy = function(handler) { this.ondestroyhandlers.push(handler); }; /** * adds an event handler and automatically registers it to be removed if/when * the plugin is destroyed. */ kwick.prototype.addeventhandler = function($el, eventname, handler) { $el.on(eventname, handler); this.ondestroy(function() { $el.off(eventname, handler); }); }; /** * "destroys" this kwicks instance plugin by performing the following: * 1) stops any currently running animations * 2) invokes all destroy handlers * 3) clears out all style attributes on panels * 4) removes all kwicks class names from panels and container * 5) removes the 'kwicks' data value from the container */ kwick.prototype.destroy = function() { this.$timer.stop(); for (var i = 0, len = this.ondestroyhandlers.length; i < len; i++) { this.ondestroyhandlers[i](); } this.$panels .attr('style', '') .removeclass('kwicks-expanded kwicks-selected kwicks-collapsed'); this.$container // note: kwicks and kwicks- classes have extra smarts around them // back in the constructor .removeclass('kwicks-processed') .removedata('kwicks'); }; /** * forces the panels to be updated in response to the container being resized. */ kwick.prototype.resize = function() { // bail out if container size hasn't changed if (this.getcontainersize() === this.getcontainersize(true)) return; this.calculatepanelsizes(); this.offsets = this.getoffsetsforexpanded(); // if the panels are currently being animated, we'll just set a flag that can be detected // during the next animation step if (this.isanimated) { this._dirtyoffsets = true; } else { // otherwise update the styles immediately this.updatepanelstyles(); } }; /** * selects the panel with the specified index (use -1 to select none) */ kwick.prototype.select = function(index) { // make sure the panel isn't already selected if (index === this.selectedindex) return; $(this.getselectedpanel()).removeclass('kwicks-selected'); this.selectedindex = index; $(this.getselectedpanel()).addclass('kwicks-selected'); }; /** * expands the panel with the specified index (use -1 to expand none) */ kwick.prototype.expand = function(index) { var self = this, // used for expand-complete event later on oldindex = this.expandedindex, oldexpanded = this.getexpandedpanel(); // if the index is -1, then default it to the currently selected index (which will also be // -1 if no panels are currently selected) if (index === -1) index = this.selectedindex; // make sure the panel isn't already expanded if (index === this.expandedindex) return; $(this.getexpandedpanel()).removeclass('kwicks-expanded'); $(this.getcollapsedpanels()).removeclass('kwicks-collapsed'); this.expandedindex = index; $(this.getexpandedpanel()).addclass('kwicks-expanded'); $(this.getcollapsedpanels()).addclass('kwicks-collapsed'); // handle panel animation var $timer = this.$timer, numpanels = this.$panels.length, startoffsets = this.offsets.slice(), offsets = this.offsets, targetoffsets = this.getoffsetsforexpanded(); $timer.stop()[0].progress = 0; this.isanimated = true; $timer.animate({ progress: 1 }, { duration: this.opts.duration, easing: this.opts.easing, step: function(progress) { // check if we've resized mid-animation (yes, we're thorough) if (self._dirtyoffsets) { offsets = self.offsets; targetoffsets = self.getoffsetsforexpanded(); self._dirtyoffsets = false; } offsets.length = 0; for (var i = 0; i < numpanels; i++) { var targetoffset = targetoffsets[i], newoffset = targetoffset - ((targetoffset - startoffsets[i]) * (1 - progress)); offsets[i] = newoffset; } self.updatepanelstyles(); }, complete: function() { self.isanimated = false; self.$container.trigger('expand-complete.kwicks', { index: index, expanded: self.getexpandedpanel(), collapsed: self.getcollapsedpanels(), oldindex: oldindex, oldexpanded: oldexpanded, // note: this will always be false but is included to match expand event isanimated: false }); } }); }; })(jquery);