/*! * kwicks: sexy sliding panels for jquery - v2.1.0 * http://devsmash.com/projects/kwicks * * copyright 2013 jeremy martin (jmar777) * contributors: duke speer (duke3d) * 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 = { maxsize: -1, minsize: -1, spacing: 5, duration: 500, isvertical: false, easing: undefined, behavior: null, autoresize: 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') throw new error('unrecognized kwicks behavior specified: ' + o.behavior); $.each(['minsize', 'maxsize'], 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) { return this.each(function() { var $this = $(this), $panel; // if this is a container, then we require a panel index if ($this.is('.kwicks-processed')) { if (typeof index !== 'number') throw new error('kwicks method "expand" requires an index'); // protect against jquery's eq(-index) feature if (index >= 0) $panel = $this.children().eq(index); } // otherwise `this` should be a panel already else if ($this.parent().is('.kwicks-processed')) { // don't need panel in this scenario $panel = $this; index = $panel.index(); } // if it's not a container or a panel, then this was an erroneous method call else { throw new error('cannot call "expand" method on a non-kwicks element'); } // try to trigger on panel, but default to container if panel doesn't exist var $target = ($panel && $panel.length) ? $panel : $this; $target.trigger('expand.kwicks', { index: index }); }); }, expanded: function() { var kwick = this.first().data('kwicks'); if (!kwick) throw new error('cannot called "expanded" method on a non-kwicks element'); return kwick.expandedindex; }, select: function(index) { return this.each(function() { var $this = $(this), $panel; // if this is a container, then we require a panel index if ($this.is('.kwicks-processed')) { if (typeof index !== 'number') throw new error('kwicks method "select" requires an index'); // protect against jquery's eq(-index) feature if (index >= 0) $panel = $this.children().eq(index); } // otherwise `this` should be a panel already else if ($this.parent().is('.kwicks-processed')) { // don't need panel in this scenario $panel = $this; index = $panel.index(); } // if it's not a container or a panel, then this was an erroneous method call else { throw new error('cannot call "expand" method on a non-kwicks element'); } // try to trigger on panel, but default to container if panel doesn't exist var $target = ($panel && $panel.length) ? $panel : $this; $target.trigger('select.kwicks', { index: index }); }); }, selected: function() { var kwick = this.first().data('kwicks'); if (!kwick) throw new error('cannot called "selected" method on a non-kwicks element'); return kwick.selectedindex; }, resize: function(index) { return this.each(function() { var $this = $(this), kwick = $this.data('kwicks'); if (!kwick) { throw new error('cannot called "resize" method on a non-kwicks element'); } kwick.resize(); }); } }; /** * 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 $el = $(e.target); var kwick = $el.data('kwicks') || $el.parent().data('kwicks'); // should we throw here? if (!kwick) return; 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 $el = $(e.target); var kwick = $el.data('kwicks') || $el.parent().data('kwicks'); // should we throw here? if (!kwick) return; kwick.select(data.index); } }; /** * instantiates a new kwick instance using the provided container and options. */ var kwick = function kwick(container, opts) { this.opts = opts; // references to our dom elements var orientation = opts.isvertical ? 'vertical' : 'horizontal'; this.$container = $(container).addclass('kwicks').addclass('kwicks-' + orientation); this.$panels = this.$container.children(); // 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 }); // the current offsets for each panel this.offsets = this.getoffsetsforexpanded(); this.initstyles(); this.initbehavior(); this.initwindowresizehandler(); }; /** * calculates size, minsize, and maxsize based on the current size of the container and the * user-provided options. the results will be stored on this.panelsize, this.panelminsize, and * this.panelmaxsize. 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, numpanels = this.$panels.length, containersize = this.getcontainersize(true), sumspacing = opts.spacing * (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.opts.spacing, size = this.panelsize, minsize = this.panelminsize, maxsize = this.panelmaxsize; if(minsize>maxsize){ minsize = this.panelmaxsize; size = this.panelmaxsize; this.panelminsize=this.panelmaxsize; this.panelsize=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.opts.spacing, 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; } }; /** * sets initial styles on the container element and panels */ kwick.prototype.initstyles = function() { var opts = this.opts, $container = this.$container, $panels = this.$panels, numpanels = $panels.length, pdim = this.primarydimension, sdim = this.secondarydimension; this.updatepanelstyles(); }; /** * 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; var $container = this.$container; switch (this.opts.behavior) { case 'menu': this.$container.on('mouseleave', function() { $container.kwicks('expand', -1); }).children().on('mouseover', function() { $(this).kwicks('expand'); }).click(function() { $(this).kwicks('select'); }); break; default: throw new error('unrecognized behavior option: ' + this.opts.behavior); } }; /** * 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; 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(); } $(window).on('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](); //var width=document.body.clientwidth; var width=$(".kwicks").width(); containersize = this._containersize =width; //if(document.body.clientwidth>window.screen.width){width=window.screen.width;} } return containersize; }; /** * gets a reference to the currently expanded panel (if there is one) */ kwick.prototype.getexpandedpanel = function() { return this.expandedindex === -1 ? $([]) : this.$panels.eq(this.expandedindex); }; /** * gets a reference to the currently selected panel (if there is one) */ kwick.prototype.getselectedpanel = function() { return this.selectedindex === -1 ? $([]) : this.$panels.eq(this.selectedindex); }; /** * forces the panels to be updated in response to the container being resized. */ kwick.prototype.resize = function(index) { // 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 (and expands) 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) { // it's possible through the api to have a panel already selected but not expanded, // so ensure that the panel really is expanded return this.expand(index); } this.getselectedpanel().removeclass('kwicks-selected'); this.selectedindex = index; this.getselectedpanel().addclass('kwicks-selected'); this.expand(index); }; /** * expands the panel with the specified index (use -1 to expand none) */ kwick.prototype.expand = function(index) { var self = this; // 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.expandedindex = index; this.getexpandedpanel().addclass('kwicks-expanded'); // 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) { 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; } }); }; })(jquery);