Copy
/** * jQuery Collapsable - jQuery plugin for collapsable boxes * http://zipper.github.io/jquery.collapsable/ * * @author Radek Šerý <[email protected]> * @copyright Copyright (c) 2014-2016 Radek Šerý * @license MIT * * @version 2.0.2 */;(function($) { // @feature: díky předávání collapsableEvent do expand.collapsable (atd.) je možné použít e.collapsableEvent.preventDefault() místo defaults.preventDefault! cool, ne?! /** * Collapsable defaults * @type {{control: string, box: string, event: string, fx: boolean, fxDuration: number, accordion: boolean, collapsableAll: boolean, preventDefault: boolean, extLinks: {selector: null, preventDefault: boolean, activeClass: string}, classNames: {expanded: string, collapsed: string, defaultExpanded: string}}} */ var defaults = { control: '.ca-control', // CSS selector for control element box: '.ca-box', // CSS selector for hideable element (box) event: 'click', // event triggering the expand/collapse preventDefault: true, // whether prevenDefault should be called when specified event occurs on control; even if false, e.collapsableEvent.preventDefault() may be used inside collapsable event handlers fx: null, // effect for expanding/collapsing, [ false | toggle | slide | fade | {Object} ] fxDuration: 0, // duration of the effect, affects delay between `expand.collapsable`(`collapse.collapsable`) and `expanded.collapsable` (`collapsed.collapsable`) evetns are triggered; default value is 500 when fx set to slide accordion: false, // determines, if there could be more than one expanded box in same time; related to jQuery set on which initialized collapsableAll: true, // possibility of collapsing all boxes from set extLinks: { // external links for operating collapsable set, can be anywhere else in DOM selector: '', // CSS selector for external links; it has to be anchors; the click event is binded preventDefault: false // whether preventDefault is called on extLinks click }, classNames: { // CSS class names to be used on collapsable box; they are added to element, on which collapsable has been called expanded: 'ca-expanded', collapsed: 'ca-collapsed', defaultExpanded: 'ca-default-expanded', extLinkActive: 'ca-ext-active' } // callbacks are no more available, use event handlers instead }; /** * Public methods available via jQuery adapter * @type {{init: methods.init, expandAll: methods.expandAll, collapseAll: methods.collapseAll, destroy: methods.destroy}} */ var methods = { init: function (options) { return new Collapsable(this, options); }, expandAll: function (data) { handlePublicMethods.call(this, 'expandAll', data); }, collapseAll: function (data) { handlePublicMethods.call(this, 'collapseAll', data); }, destroy: function (data) { handlePublicMethods.call(this, 'destroy', data); } }; /** * Last used uid index * @type {number} * @private */ var caUid = 0; /** * Generates unique id for new Collapsable object * @returns {String} New uid * @private */ function getUid() { return 'ca-uid-' + caUid++; } /** * Handles public method called on jQuery object using adapter * @param {String} action - collapseAll|expandAll|destroy * @param {Object} data - Data passed by user * @private */ function handlePublicMethods(action, data) { var processed = []; this.each(function () { var instance = $(this).data('collapsable'); if (instance) { var uid = instance.parent.uid; if (processed.indexOf(uid) === -1) { processed.push(uid); instance.parent[action](data); } } }); } /** * Finds ext links specified in options and bind click event to them for opening * @this Collapsable * @todo create possibility for extLinks would only open the boxes? for now, it might be achieved using event handlers and e.preventDefault * @private */ function handleExtLinks() { var opts = this.opts; if ((this.$extLinks = $(opts.extLinks.selector).filter('a')).length) { this.$extLinks.on('click.collapsable', function (event) { if (opts.extLinks.preventDefault) event.preventDefault(); var collapsable = $($(this).attr('href')).data('collapsable'); if (collapsable) { collapsable.$controlLink.first().trigger(opts.event + '.collapsable', event); } }); } } /** * Prepare default expanded item. Checks the necessity of expanded item (collapsableAll set to false && accordion set * to true) and limits amount of default expanded items to 1 when accordion option set to true. Also when there's * fragment in url targeting existing collapsable item, default expanded set using class in DOM will be overridden. * * @summary Sets the defaultExpanded flag to appropriate CollapsableItems * @this Collapsable * @private */ function prepareDefaultExpanded() { var fragment = window.location.href; var i = fragment.search(/#/); var defaultExpanded = -1; var defaultExpandedFromUrl = -1; var $items = $($.map(this.items, function (item) { return item.$collapsable.get(); })); // search for #hash in url if (i !== -1) { fragment = fragment.substring(i + 1); defaultExpandedFromUrl = $items.index($('#' + fragment)); if (defaultExpandedFromUrl !== -1) { this.items[defaultExpandedFromUrl].defaultExpanded = true; // max 1, we can return now if (this.opts.accordion) { return; } } } // max 1 expanded item if (this.opts.accordion) { defaultExpanded = $items.index($('.' + this.opts.classNames.defaultExpanded)); // max 1, we can return now if (defaultExpanded !== -1) { this.items[defaultExpanded].defaultExpanded = true; return; } } // not accordion, we add flag to all items with class else { var that = this; $items.each(function (i) { if ($(this).hasClass(that.opts.classNames.defaultExpanded)) { defaultExpanded = i; // for later use it is sufficient to have last index only that.items[i].defaultExpanded = true; } }); } // if we need one and none was found yet, we take the first if (defaultExpandedFromUrl === -1 && defaultExpanded === -1 && !this.opts.collapsableAll) { this.items[0].defaultExpanded = true; } } /** * Expand or collapse item based on flags set in prepareDefaultExpanded method; called on initialization within the * context of Collapsable * @todo When !opts.collapseAll && opts.accordion, we now force-open the first item with defaultExpanded flag, regardless how it got it (hash in url or class); potentially force-open the one from URL instead of first, if hash set? or maybe try to open some without forcing and only if failed, do force-open (would require two passes)? * @this Collapsable * @private */ function handleDefaultExpanded() { var event = $.Event('init.collapsable'); var opts = this.opts; var items = this.items; // save fx so it can be set back var fx = opts.fx; // for initialization, we don't want to use any effect if (opts.fx) opts.fx = 'toggle'; var l = items.length; var force = !opts.collapsableAll; // if we can't collapse all, we force expanding the first one chosen in prepareDefaultExpanded for (var i = 0; i < l; i++) { if (items[i].defaultExpanded) { items[i].expand(event, null, force); force = false; } else { items[i].collapse(event, null, true); // on init, we want to force-close all - if false returned, than the class might have been set in first place (so it would go into if statement above) } } opts.fx = fx; } /** * It is possible to set the `fx` options to `slide` or `fade` - these are shortcuts for objects with * `{ expand: 'slideDown', collapse: 'slideUp' }` or `{ expand: 'fadeIn', collapse: 'fadeOut' }`. This function * converts those strings into objects. It also sets default duration for these effects to 500ms. * @this Collapsable * @private */ function prepareFxOpt() { var opts = this.opts; // default fxDuration in case of slide function if (opts.fx === 'slide' || opts.fx === 'fade') { opts.fxDuration = opts.fxDuration || 0; } // convert alias into objects if (opts.fx === 'slide') { opts.fx = { expand: 'slideDown', collapse: 'slideUp' }; } else if (this.opts.fx === 'fade') { opts.fx = { expand: 'fadeIn', collapse: 'fadeOut' }; } } /** * Represents set of collapsable elements which were initialized by one call with same options * @name Collapsable * @param {jQuery} $boxSet - Set of object to be initilized * @param {Object} options - See plugin defaults * @returns {Collapsable} * @constructor */ var Collapsable = function ($boxSet, options) { this.opts = $.extend(true, {}, $.fn.collapsable.defaults, options); this.items = []; var that = this; this.uid = getUid(); this.promiseOpen = false; handleExtLinks.call(this); prepareFxOpt.call(this); $boxSet.each(function () { var collapsable = new CollapsableItem(that, this); if (collapsable.$box.length && collapsable.$control.length) { that.items.push(collapsable); } }); prepareDefaultExpanded.call(this); handleDefaultExpanded.call(this); return this; }; /** * Returns indexes of expanded items in Collapsable.items * @returns {Array} */ Collapsable.prototype.getExpanded = function () { var expanded = []; var l = this.items.length; for (var i = 0; i < l; i++) { if (this.items[i].isExpanded) { expanded.push(i); } } return expanded; }; /** * Expands all collapsed items * @param {Object} data - Data to be passed to triggered event */ Collapsable.prototype.expandAll = function (data) { // if accordion, we only want to expand one (first) box, or none if already expanded if (this.opts.accordion && this.getExpanded().length) { return; } var event = $.Event('expandAll.collapsable'); var l = this.items.length; for (var i = 0; i < l; i++) { if (! this.items[i].isExpanded) { var expanded = this.items[i].expand(event, data); if (this.opts.accordion && expanded) { break; } } } }; /** * Collapses all expanded items * @param {Object} data - Data to be passed to triggered event */ Collapsable.prototype.collapseAll = function (data) { var event = $.Event('collapseAll.collapsable'); var expandedItems = this.getExpanded(); var l = expandedItems.length; for (var i = 0; i < l; i++) { this.items[expandedItems[i]].collapse(event, data); } }; /** * Destroy collapsable and reverts DOM changes to state prior initialization */ Collapsable.prototype.destroy = function () { var opts = this.opts; var l = this.items.length; for (var i = 0; i < l; i++) { var item = this.items[i]; // remove classes and event handlers from main element item.$collapsable .removeClass(opts.classNames.collapsed + ' ' + opts.classNames.expanded) .removeData('collapsable'); // revert control element item.$control.find('[data-ca-created]').each(function () { $(this).parent().html($(this).html()); }); item.$control .find('.ca-link').addBack('.ca-link') .off(opts.event + '.collapsable') .removeClass('ca-link') .removeAttr('aria-controls') .removeAttr('aria-expanded'); // revert box element var style = item.$box.data('ca-pre-init-style') || ''; item.$box .attr('style', style) .removeData('ca-pre-init-style') .removeAttr('aria-hidden'); item.$collapsable.trigger('destroy.collapsable'); } // remove this.$extLinks.off('click.collapsable'); }; /** * Prepares DOM structure for collapsable element. Each ca-control element is tested for being anchor. If there's * match, the element (anchor) is added class `ca-link`. If no match, we try to find anchor inside and add class * to each of them as well. If ca-control is not an anchor and contains no anchor, we wrap inside to anchor with * appropriate class and custom data attribute for potential destroy handle. It also stores default style attribute * of box element for later use in destroy handle. * @summary Prepares DOM structure for collapsable element * @this CollapsableItem * @private */ function prepareCollapsableDOM() { var collapsableItem = this; if (! collapsableItem.id) { collapsableItem.id = getUid(); collapsableItem.$collapsable.attr('id', this.id); } var boxId = collapsableItem.$box.attr('id') || collapsableItem.id + '-ca-box'; collapsableItem.$box .attr('id', boxId) .data('ca-pre-init-style', collapsableItem.$box.attr('style')); collapsableItem.$control.each(function() { var $el = $(this); var $a; // a.ca-control -> add class .ca-link if ($el.is('a')) { $a = $el; } // .ca-control a -> add class .ca-link else if (($a = $el.find('a')).length) { } // no anchor found, create custom else { $el.wrapInner('<a data-ca-created href="#' + collapsableItem.id + '"></a>'); $a = $el.find('a'); } $a.addClass('ca-link'); $a.attr('aria-controls', boxId); }); } /** * Single item in Collapsable object, represents one collapsable element in page * @param {Collapsable} parent - Reference to group of Collapsable elements initialized in same time, sharing same options * @param {jQuery} element - One instance of collapsable element * @returns {CollapsableItem} * @constructor */ var CollapsableItem = function(parent, element) { this.parent = parent; this.$collapsable = $(element); this.id = this.$collapsable.attr('id'); this.$control = this.$collapsable.find(parent.opts.control); this.$box = this.$collapsable.find(parent.opts.box); if (this.$control.length == 0 || this.$box.length == 0) return; var that = this; var opts = this.parent.opts; // shortcut prepareCollapsableDOM.call(this); this.$controlLink = this.$control.find('.ca-link').addBack('.ca-link'); // collapsableEvent contains arguments passed when trigger is called, used for passing event that triggered opening (eg. extLink click) this.$controlLink.on(opts.event + '.collapsable', function(event, collapsableEvent) { var passEvent = collapsableEvent ? collapsableEvent : event; if (opts.preventDefault) { event.preventDefault(); } if (that.isExpanded) { that.collapse(passEvent); } else { that.expand(passEvent); } }); this.$collapsable.data('collapsable', this); return this; }; /** * Handling common parts of expanding and collapsing * @param {String} action - Either `expand` or `collapse` * @param {Object} data - Data passed to triggered event * @param {Event} collapsableEvent - Event to be passed to event handlers * @returns {boolean} */ function handleExpandCollapse(action, collapsableEvent, data) { var opts = this.parent.opts; var that = this; var trigger = 'expanded'; var addClass = opts.classNames.expanded; var removeClass = opts.classNames.collapsed; // capitalize first letter if (action === 'collapse') { trigger = 'collapsed'; addClass = opts.classNames.collapsed; removeClass = opts.classNames.expanded; } var event = $.Event(trigger + '.collapsable', { customData: data, collapsableEvent: collapsableEvent }); this.isExpanded = action === 'expand'; // update extLinks this.parent.$extLinks .filter('[href="#' + this.id + '"]') [action === 'expand' ? 'addClass' : 'removeClass'](opts.classNames.extLinkActive); // aria support this.$controlLink.attr('aria-expanded', action === 'expand'); this.$box.attr('aria-hidden', action !== 'expand'); // actually toggle the box state if(opts.fx && typeof opts.fx === 'object') { this.$box[opts.fx[action]](opts.fxDuration, function () { that.$collapsable.trigger(event); }); } else { if(opts.fx === 'toggle') { this.$box[action === 'expand' ? 'show' : 'hide'](); } setTimeout(function () { that.$collapsable.trigger(event); }, opts.fxDuration); } // update classes on collapsable element itself this.$collapsable .removeClass(removeClass) .addClass(addClass); return true; } /** * Expands single CollapsableItem; could be prevented by `preventDefault` called on `expand.collapsable` event * @param {Object} collapsableEvent - Event passed to triggered event * @param {Object} data - Data passed to triggered event * @param {Boolean} force - Forcing CollapsableItem to expand regardless on onExpand return value, should be used only on initilization (force open default expanded item when collapsableAll === false) * @returns {Boolean} - Returns if CollapsableItem has been expanded or not */ CollapsableItem.prototype.expand = function(collapsableEvent, data, force) { var opts = this.parent.opts; var expandedItem = this.parent.getExpanded(); // accordion -> max one expanded item this.parent.promiseOpen = true; // allows us to collapse expanded item even if there might be collapseAll === false option if (opts.accordion) { // before expanding, we have to collapse previously opened item, if accordion element hasn't collapsed, we can't continue if (expandedItem.length && this.parent.items[expandedItem[0]].collapse(collapsableEvent, data, force) === false) { this.parent.promiseOpen = false; return false; } } this.parent.promiseOpen = false; var event = $.Event('expand.collapsable', { customData: data, collapsableEvent: collapsableEvent }); this.$collapsable.trigger(event); if (event.isDefaultPrevented() && ! force) { // collapsableAll === false && accordion === true -> if the box has not opened, we must make sure something is opened, therefore we force-open previously opened box (opts.accordion is true means we tried to collapse something), simulating it has never closed in first place if (! opts.collapsableAll && opts.accordion) { this.parent.items[expandedItem[0]].expand(collapsableEvent, data, true); } return false; } return handleExpandCollapse.call(this, 'expand', collapsableEvent) }; /** * Collapses single CollapsableItem; could be prevented by `preventDefault` called on `collapse.collapsable` event * @param {Object} collapsableEvent - Event passed to triggered event * @param {Object} data - Data passed to triggered event * @param {Boolean} force - Forcing CollapsableItem to collapse regardless on onCollapse return value * @returns {Boolean} - Returns if CollapsableItem has been collapsed or not */ CollapsableItem.prototype.collapse = function(collapsableEvent, data, force) { var opts = this.parent.opts; // if we can't collapse all, we are not promised to open something and there is only one opened box, then we can't continue if (! opts.collapsableAll && ! this.parent.promiseOpen && this.parent.getExpanded().length < 2) { return false; } var event = $.Event('collapse.collapsable', { customData: data, collapsableEvent: collapsableEvent }); this.$collapsable.trigger(event); if (event.isDefaultPrevented() && !force) { return false; } return handleExpandCollapse.call(this, 'collapse', collapsableEvent); }; /** * The jQuery plugin namespace. * @external "jQuery.fn" * @see {@link http://learn.jquery.com/plugins The jQuery Plugin Guide} */ /** * jQuery adapter for Collapsable object, returns elements on which it was called, so it's chainable * @function external:"jQuery.fn".collapsable * @param {Object} options - Options to override plugin defaults * @returns {jQuery} */ $.fn.collapsable = function(options) { if (methods[options]) { return methods[options].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof options === 'object' || !options) { methods.init.apply(this, arguments); return this; } else { $.error('Method ' + options + ' does not exist on jQuery.collapsable'); } }; $.fn.collapsable.defaults = defaults;})(jQuery);
|
답변 1개
채택된 답변
+20 포인트
2016-03-14 (월) 18:06:09
소스가 길어 다 보지는 못했지만 속도문제는
다음을 수정하면 될 듯 보이네요.
var defaults 객체의
fxDuration: 0 을 1초면 1000 0.5초면 500 등 밀리세컨드로 수정하셔 시도해 보세요.
답변을 작성하려면 로그인이 필요합니다.