From 675ea8cdbb0b5ed92a5da192c75f85398e6681d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20=C5=A0er=C3=BD?= Date: Sun, 21 Feb 2016 23:47:01 +0100 Subject: [PATCH 01/10] #0 v2.0.0 - wip --- jquery.collapsable.2.0.0.js | 307 ++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 jquery.collapsable.2.0.0.js diff --git a/jquery.collapsable.2.0.0.js b/jquery.collapsable.2.0.0.js new file mode 100644 index 0000000..a089250 --- /dev/null +++ b/jquery.collapsable.2.0.0.js @@ -0,0 +1,307 @@ +;(function($) { + + var defaults = { + control: '.ca-control', // selektor ovladaciho prvku, muze se v ramci boxu i opakovat + box: '.ca-box', // selektor pro skryvanou/zobrazovanou cast + event: 'click', // událost, na kterou je u ca-control bindované otevírání + + fx: false, // [ false (jen zmena class) / toggle (prepinani show/hide) / slide (animace vysky) ] + fxDuration: 0, // doba trvání efektu, případně zpoždění volání onExpanded a onCollapsed funkcí; výchozí hodnota 500, pokud je fx === 'slide' + + grouped: false, // pokud je true, pak bude otevreny vzdy jen jeden box ze skupiny + collapsableAll: true, // lze zavrit vsechny polozky, ma vliv pouze pokud grouped==true + defaultOpen: null, // otevreny/zavreny po inicializaci + preventDefault: true, // po kliku na control zavolani / nezavolani preventDefault na event + + extLinks: { + selector: null, // false nebo selektor externich odkazu (napr. prichycen horni lista, atp.) + preventDefault: false, // true / false zda po kliku na externí odkaz má být zavoláno preventDefault na události click + activeClass: 'ca-ext-active' + }, + expandAll: null, // false nebo selektor na odkaz, ktery otevre vsechny boxy + collapseAll: null, // false nebo selektor na odkaz, ktery zavre vsechny boxy + + classNames: { // nazvy trid, otevreny, zavreny box a box, ktery bude defaultne otevreny + expanded: 'ca-expanded', + collapsed: 'ca-collapsed', + defaultExpanded: 'ca-default-expanded' + }, + + onInit: null, // callback funkce volana ihned po inicializaci + onExpand: null, // callback volany pred samotny oteviranim + onExpanded: null, // callback zavolany po dokonceni otevirani, ma smysl jen pri fx: 'slide' s nenulovym casem + onCollapse: null, // analogicky viz vyse + onCollapsed: null + }; + + $.fn.collapsable = function(options) { + if (methods[options]) { + return methods[options].apply(this, Array.prototype.slice.call(arguments, 1)); + } + else if (typeof options === 'object' || !options) { + var ret = methods.init.apply( this, arguments ); + + var data = ret.data('collapsable'); + if (data && typeof data.parent.opts.onInit == 'function') { + $(this).each(function() { + data.parent.opts.onInit.call($(this)); + }); + } + + return ret; + } + else { + $.error('Method ' + options + ' does not exist on jQuery.collapsable' ); + } + }; + + $.fn.collapsable.defaults = defaults; + + // public methods which can be used via .collapsable('method'); + var methods = { + init: function(options) { + return new Collapsable(this, options); + }, + expandAll: function() {}, + collapseAll: function() {}, + destroy: function() {} + }; + + var Collapsable = function($boxSet, options) { + this.opts = $.extend(true, {}, $.fn.collapsable.defaults, options); + this.items = []; + this.$boxSet = $boxSet; + this.$extLinks = this.opts.extLinks.selector ? $(this.opts.extLinks.selector) : $([]); + + var that = this; + var fragment = window.location.href; + + // search for #hash in url + if ((i = fragment.search(/#/)) != -1) { + fragment = fragment.substring(i+1); + } else { + fragment = ''; + } + + // default fxDuration in case of slide function + if (this.opts.fx === 'slide' && ! this.opts.fxDuration) { + this.opts.fxDuration = 500; + } + + if (this.$extLinks.length) { + // we assume same structure among all $extLinks! + if (this.$extLinks[0].tagName.toUpperCase() != 'A') + this.$extLinks = this.$extLinks.find('a'); + + this.$extLinks.on('click', function(e) { + var $target = $($(this).attr('href')); + + if (that.opts.extLinks.preventDefault) + e.preventDefault(); + + if ($target.hasClass(that.opts.classNames.collapsed)) { + var $control = $target.find(that.opts.control); + var $btn = ($control[0].tagName.toUpperCase() == 'A') ? $control : $control.find('a'); + + $btn.trigger('click'); + } + }); + } + + if (this.opts.expandAll) { + $(this.opts.expandAll).bind('click', function(e) { + e.preventDefault(); + methods.expandAll.call(that.$boxSet, e); + }); + } + + if (this.opts.collapseAll) { + $(opts.collapseAll).bind('click', function(e) { + e.preventDefault(); + methods.collapseAll.call(that.$boxSet, e); + }); + } + + if (this.opts.grouped && ! this.opts.collapsableAll && (fragment === '' || this.$boxSet.filter('#' + fragment).length === 0)) { + var $visible = this.$boxSet.filter('.' + this.opts.classNames.defaultExpanded); + if ($visible.length == 0) + this.$boxSet.first().addClass(this.opts.classNames.defaultExpanded); + } + + if ((fragment && ($fragment = this.$boxSet.filter('#' + fragment)).length)) { + this.$boxSet + .filter('.' + this.opts.classNames.defaultExpanded) + .removeClass(this.opts.classNames.defaultExpanded); + $fragment.addClass(this.opts.classNames.defaultExpanded); + } + + return this.$boxSet.each(function() { + var collapsable = new CollapsableItem(that, this) + + if (collapsable.$box.length && collapsable.$control.length) { + that.items.push(collapsable); + } + }); + }; + + Collapsable.prototype.findExpanded = function() { + var l = this.items.length; + for (var i = 0; i < l; i++) { + if (this.items[i].isExpanded()) + return this.items[i]; + } + return null; + }; + + /***** CollapsableItem - one instance of collapsable element ****/ + var CollapsableItem = function(parent, collapsable) { + // public + this.parent = parent; + this.$collapsable = $(collapsable); + this.id = this.$collapsable.attr('id'); + this.$control = this.$collapsable.find(parent.opts.control); + this.$box = this.$collapsable.find(parent.opts.box); + + // private + var that = this; + var $anchor; // clickable element + var opts = this.parent.opts; // shortcut + var spinnerExt = $.nette ? $.nette.ext('spinner') : null; + + if (! this.id) { + this.id = 'collapsable-' + (new Date()).getTime(); + this.$collapsable.attr('id', this.id); + } + + + if (this.$control.length == 0 || this.$box.length == 0) + return; + + if (this.$control.find('a').length === 0 && this.$control[0].tagName.toUpperCase() != 'A') { + this.$control.wrapInner(''); + } + + $anchor = (this.$control[0].tagName.toUpperCase() === 'A') ? this.$control : this.$control.find('a'); + + if (opts.defaultOpen || this.$collapsable.hasClass(opts.classNames.defaultExpanded)) { + this.expand(); + } + else { + if (opts.fx !== false) this.$box.hide(); + this.$collapsable + .removeClass(opts.classNames.expanded) + .addClass(opts.classNames.collapsed); + } + + this.$collapsable.on(opts.event, $anchor, function(e) { + if (opts.preventDefault) { + e.preventDefault(); + } + + if (that.isExpanded()) { + if (opts.collapsableAll || ! opts.grouped) { + var collapsed = that.collapse(e); + } + } + else { + if (opts.grouped) { + var expandedItem = that.parent.findExpanded(); + var hasCollapsed; + if (expandedItem) { + hasCollapsed = expandedItem.collapse(e); + + if (hasCollapsed === false) { + return; + } + } + } + + var hasExpanded = that.expand(e); + + if (hasExpanded !== false && $(this).hasClass('ajax') && spinnerExt && spinnerExt.$spinnerHtml) { + that.$box.append(spinnerExt.$spinnerHtml); + } + } + }); + + this.$collapsable.data('collapsable', that); + }; + + CollapsableItem.prototype.expand = function(event) { + var opts = this.parent.opts; + if(typeof opts.onExpand == 'function') { + var expand = opts.onExpand.call(this.$collapsable, event); + if (expand === false) + return false; + } + + this.parent.$extLinks + .filter('[href=#' + this.$collapsable.attr('id') + ']') + .addClass(opts.classNames.extLinkActive); + + this.$collapsable + .removeClass(opts.classNames.collapsed) + .addClass(opts.classNames.expanded); + + if(opts.fx == 'slide') { + this.$box + .slideDown(opts.fxDuration, function() { + if(typeof opts.onExpanded == 'function') opts.onExpanded.call(this.$collapsable, event); + }) + .css({ display: 'block' }); + } + else { + if(opts.fx == 'toggle') { + this.$box.show(); + } + + if(typeof opts.onExpanded == 'function') { + t = setTimeout(function () { + opts.onExpanded.call(this.$collapsable, event); + }, opts.fxDuration); + } + } + }; + + CollapsableItem.prototype.collapse = function(event) { + var opts = this.parent.opts; + if(typeof opts.onCollapse == 'function') { + var collapse = opts.onCollapse.call(this.$collapsable, event); + if (collapse === false) { + return false; + } + } + + this.parent.$extLinks + .filter('[href=#' + this.$collapsable.attr('id') + ']') + .removeClass(opts.classNames.extLinkActive); + + this.$collapsable + .removeClass(opts.classNames.expanded) + .addClass(opts.classNames.collapsed); + + if(opts.fx == 'slide') { + this.$box + .css({ display: 'block' }) + .slideUp(opts.fxDuration, function () { + if (typeof opts.onCollapsed == 'function') opts.onCollapsed.call(this.$collapsable, event); + }); + } + else { + if(opts.fx == 'toggle') { + this.$box.hide(); + } + + if(typeof opts.onCollapsed == 'function') { + t = setTimeout(function () { + opts.onCollapsed.call(this.$collapsable, event); + }, opts.fxDuration); + } + } + }; + + CollapsableItem.prototype.isExpanded = function() { + return this.$collapsable.hasClass(this.parent.opts.classNames.expanded); + }; + +})(jQuery); From 79a9a398a862d9ac8bd9f6e37242876e5ada5711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20=C5=A0er=C3=BD?= Date: Mon, 22 Feb 2016 19:34:43 +0100 Subject: [PATCH 02/10] #0 v2.0.0 - wip --- jquery.collapsable.2.0.0.js | 307 -------------------- jquery.collapsable.js | 539 +++++++++++++++++++++--------------- 2 files changed, 319 insertions(+), 527 deletions(-) delete mode 100644 jquery.collapsable.2.0.0.js diff --git a/jquery.collapsable.2.0.0.js b/jquery.collapsable.2.0.0.js deleted file mode 100644 index a089250..0000000 --- a/jquery.collapsable.2.0.0.js +++ /dev/null @@ -1,307 +0,0 @@ -;(function($) { - - var defaults = { - control: '.ca-control', // selektor ovladaciho prvku, muze se v ramci boxu i opakovat - box: '.ca-box', // selektor pro skryvanou/zobrazovanou cast - event: 'click', // událost, na kterou je u ca-control bindované otevírání - - fx: false, // [ false (jen zmena class) / toggle (prepinani show/hide) / slide (animace vysky) ] - fxDuration: 0, // doba trvání efektu, případně zpoždění volání onExpanded a onCollapsed funkcí; výchozí hodnota 500, pokud je fx === 'slide' - - grouped: false, // pokud je true, pak bude otevreny vzdy jen jeden box ze skupiny - collapsableAll: true, // lze zavrit vsechny polozky, ma vliv pouze pokud grouped==true - defaultOpen: null, // otevreny/zavreny po inicializaci - preventDefault: true, // po kliku na control zavolani / nezavolani preventDefault na event - - extLinks: { - selector: null, // false nebo selektor externich odkazu (napr. prichycen horni lista, atp.) - preventDefault: false, // true / false zda po kliku na externí odkaz má být zavoláno preventDefault na události click - activeClass: 'ca-ext-active' - }, - expandAll: null, // false nebo selektor na odkaz, ktery otevre vsechny boxy - collapseAll: null, // false nebo selektor na odkaz, ktery zavre vsechny boxy - - classNames: { // nazvy trid, otevreny, zavreny box a box, ktery bude defaultne otevreny - expanded: 'ca-expanded', - collapsed: 'ca-collapsed', - defaultExpanded: 'ca-default-expanded' - }, - - onInit: null, // callback funkce volana ihned po inicializaci - onExpand: null, // callback volany pred samotny oteviranim - onExpanded: null, // callback zavolany po dokonceni otevirani, ma smysl jen pri fx: 'slide' s nenulovym casem - onCollapse: null, // analogicky viz vyse - onCollapsed: null - }; - - $.fn.collapsable = function(options) { - if (methods[options]) { - return methods[options].apply(this, Array.prototype.slice.call(arguments, 1)); - } - else if (typeof options === 'object' || !options) { - var ret = methods.init.apply( this, arguments ); - - var data = ret.data('collapsable'); - if (data && typeof data.parent.opts.onInit == 'function') { - $(this).each(function() { - data.parent.opts.onInit.call($(this)); - }); - } - - return ret; - } - else { - $.error('Method ' + options + ' does not exist on jQuery.collapsable' ); - } - }; - - $.fn.collapsable.defaults = defaults; - - // public methods which can be used via .collapsable('method'); - var methods = { - init: function(options) { - return new Collapsable(this, options); - }, - expandAll: function() {}, - collapseAll: function() {}, - destroy: function() {} - }; - - var Collapsable = function($boxSet, options) { - this.opts = $.extend(true, {}, $.fn.collapsable.defaults, options); - this.items = []; - this.$boxSet = $boxSet; - this.$extLinks = this.opts.extLinks.selector ? $(this.opts.extLinks.selector) : $([]); - - var that = this; - var fragment = window.location.href; - - // search for #hash in url - if ((i = fragment.search(/#/)) != -1) { - fragment = fragment.substring(i+1); - } else { - fragment = ''; - } - - // default fxDuration in case of slide function - if (this.opts.fx === 'slide' && ! this.opts.fxDuration) { - this.opts.fxDuration = 500; - } - - if (this.$extLinks.length) { - // we assume same structure among all $extLinks! - if (this.$extLinks[0].tagName.toUpperCase() != 'A') - this.$extLinks = this.$extLinks.find('a'); - - this.$extLinks.on('click', function(e) { - var $target = $($(this).attr('href')); - - if (that.opts.extLinks.preventDefault) - e.preventDefault(); - - if ($target.hasClass(that.opts.classNames.collapsed)) { - var $control = $target.find(that.opts.control); - var $btn = ($control[0].tagName.toUpperCase() == 'A') ? $control : $control.find('a'); - - $btn.trigger('click'); - } - }); - } - - if (this.opts.expandAll) { - $(this.opts.expandAll).bind('click', function(e) { - e.preventDefault(); - methods.expandAll.call(that.$boxSet, e); - }); - } - - if (this.opts.collapseAll) { - $(opts.collapseAll).bind('click', function(e) { - e.preventDefault(); - methods.collapseAll.call(that.$boxSet, e); - }); - } - - if (this.opts.grouped && ! this.opts.collapsableAll && (fragment === '' || this.$boxSet.filter('#' + fragment).length === 0)) { - var $visible = this.$boxSet.filter('.' + this.opts.classNames.defaultExpanded); - if ($visible.length == 0) - this.$boxSet.first().addClass(this.opts.classNames.defaultExpanded); - } - - if ((fragment && ($fragment = this.$boxSet.filter('#' + fragment)).length)) { - this.$boxSet - .filter('.' + this.opts.classNames.defaultExpanded) - .removeClass(this.opts.classNames.defaultExpanded); - $fragment.addClass(this.opts.classNames.defaultExpanded); - } - - return this.$boxSet.each(function() { - var collapsable = new CollapsableItem(that, this) - - if (collapsable.$box.length && collapsable.$control.length) { - that.items.push(collapsable); - } - }); - }; - - Collapsable.prototype.findExpanded = function() { - var l = this.items.length; - for (var i = 0; i < l; i++) { - if (this.items[i].isExpanded()) - return this.items[i]; - } - return null; - }; - - /***** CollapsableItem - one instance of collapsable element ****/ - var CollapsableItem = function(parent, collapsable) { - // public - this.parent = parent; - this.$collapsable = $(collapsable); - this.id = this.$collapsable.attr('id'); - this.$control = this.$collapsable.find(parent.opts.control); - this.$box = this.$collapsable.find(parent.opts.box); - - // private - var that = this; - var $anchor; // clickable element - var opts = this.parent.opts; // shortcut - var spinnerExt = $.nette ? $.nette.ext('spinner') : null; - - if (! this.id) { - this.id = 'collapsable-' + (new Date()).getTime(); - this.$collapsable.attr('id', this.id); - } - - - if (this.$control.length == 0 || this.$box.length == 0) - return; - - if (this.$control.find('a').length === 0 && this.$control[0].tagName.toUpperCase() != 'A') { - this.$control.wrapInner(''); - } - - $anchor = (this.$control[0].tagName.toUpperCase() === 'A') ? this.$control : this.$control.find('a'); - - if (opts.defaultOpen || this.$collapsable.hasClass(opts.classNames.defaultExpanded)) { - this.expand(); - } - else { - if (opts.fx !== false) this.$box.hide(); - this.$collapsable - .removeClass(opts.classNames.expanded) - .addClass(opts.classNames.collapsed); - } - - this.$collapsable.on(opts.event, $anchor, function(e) { - if (opts.preventDefault) { - e.preventDefault(); - } - - if (that.isExpanded()) { - if (opts.collapsableAll || ! opts.grouped) { - var collapsed = that.collapse(e); - } - } - else { - if (opts.grouped) { - var expandedItem = that.parent.findExpanded(); - var hasCollapsed; - if (expandedItem) { - hasCollapsed = expandedItem.collapse(e); - - if (hasCollapsed === false) { - return; - } - } - } - - var hasExpanded = that.expand(e); - - if (hasExpanded !== false && $(this).hasClass('ajax') && spinnerExt && spinnerExt.$spinnerHtml) { - that.$box.append(spinnerExt.$spinnerHtml); - } - } - }); - - this.$collapsable.data('collapsable', that); - }; - - CollapsableItem.prototype.expand = function(event) { - var opts = this.parent.opts; - if(typeof opts.onExpand == 'function') { - var expand = opts.onExpand.call(this.$collapsable, event); - if (expand === false) - return false; - } - - this.parent.$extLinks - .filter('[href=#' + this.$collapsable.attr('id') + ']') - .addClass(opts.classNames.extLinkActive); - - this.$collapsable - .removeClass(opts.classNames.collapsed) - .addClass(opts.classNames.expanded); - - if(opts.fx == 'slide') { - this.$box - .slideDown(opts.fxDuration, function() { - if(typeof opts.onExpanded == 'function') opts.onExpanded.call(this.$collapsable, event); - }) - .css({ display: 'block' }); - } - else { - if(opts.fx == 'toggle') { - this.$box.show(); - } - - if(typeof opts.onExpanded == 'function') { - t = setTimeout(function () { - opts.onExpanded.call(this.$collapsable, event); - }, opts.fxDuration); - } - } - }; - - CollapsableItem.prototype.collapse = function(event) { - var opts = this.parent.opts; - if(typeof opts.onCollapse == 'function') { - var collapse = opts.onCollapse.call(this.$collapsable, event); - if (collapse === false) { - return false; - } - } - - this.parent.$extLinks - .filter('[href=#' + this.$collapsable.attr('id') + ']') - .removeClass(opts.classNames.extLinkActive); - - this.$collapsable - .removeClass(opts.classNames.expanded) - .addClass(opts.classNames.collapsed); - - if(opts.fx == 'slide') { - this.$box - .css({ display: 'block' }) - .slideUp(opts.fxDuration, function () { - if (typeof opts.onCollapsed == 'function') opts.onCollapsed.call(this.$collapsable, event); - }); - } - else { - if(opts.fx == 'toggle') { - this.$box.hide(); - } - - if(typeof opts.onCollapsed == 'function') { - t = setTimeout(function () { - opts.onCollapsed.call(this.$collapsable, event); - }, opts.fxDuration); - } - } - }; - - CollapsableItem.prototype.isExpanded = function() { - return this.$collapsable.hasClass(this.parent.opts.classNames.expanded); - }; - -})(jQuery); diff --git a/jquery.collapsable.js b/jquery.collapsable.js index 7798255..5bd3e64 100644 --- a/jquery.collapsable.js +++ b/jquery.collapsable.js @@ -1,288 +1,387 @@ ;(function($) { - function expandBox(opts, $box, $parent) { - if(typeof opts.onExpand == 'function') - opts.onExpand.call($parent); + var defaults = { + control: '.ca-control', // selektor ovladaciho prvku, muze se v ramci boxu i opakovat + box: '.ca-box', // selektor pro skryvanou/zobrazovanou cast + event: 'click', // událost, na kterou je u ca-control bindované otevírání + + fx: false, // [ false (jen zmena class) / toggle (prepinani show/hide) / slide (animace vysky) ] + fxDuration: 0, // doba trvání efektu, případně zpoždění volání onExpanded a onCollapsed funkcí; výchozí hodnota 500, pokud je fx === 'slide' + + grouped: false, // pokud je true, pak bude otevreny vzdy jen jeden box ze skupiny + collapsableAll: true, // lze zavrit vsechny polozky, ma vliv pouze pokud grouped==true + preventDefault: true, // po kliku na control zavolani / nezavolani preventDefault na event + + extLinks: { + selector: null, // false nebo selektor externich odkazu (napr. prichycen horni lista, atp.) + preventDefault: false, // true / false zda po kliku na externí odkaz má být zavoláno preventDefault na události click + activeClass: 'ca-ext-active' + }, - if(opts.fx == 'slide') { - $box - .slideDown(opts.collapseDelay, function() { if(typeof opts.onExpanded == 'function') opts.onExpanded.call($parent) }) - .css({display: 'block'}); - } - else { - if(opts.fx == 'toggle') { - $box.show(); - } + classNames: { // nazvy trid, otevreny, zavreny box a box, ktery bude defaultne otevreny + expanded: 'ca-expanded', + collapsed: 'ca-collapsed', + defaultExpanded: 'ca-default-expanded' + }, - if(typeof opts.onExpanded == 'function') { - t = setTimeout(function () { - opts.onExpanded.call($parent); - }, opts.collapseDelay); - } - } - } + onInit: null, // callback funkce volana ihned po inicializaci + onExpand: null, // callback volany pred samotny oteviranim + onExpanded: null, // callback zavolany po dokonceni otevirani, ma smysl jen pri fx: 'slide' s nenulovym casem + onCollapse: null, // analogicky viz vyse + onCollapsed: null + }; - function collapseBox(opts, $box, $parent) { - if(typeof opts.onCollapse == 'function') - opts.onCollapse.call($parent); + // public methods which can be used via .collapsable('method') + var methods = { + init: function(options) { + var instance = new Collapsable(this, options); + return this; + }, + expandAll: function(event) { + }, + collapseAll: function(event) { + var collapsed = []; + this.each(function() { + var collapsable = $(this).data('collapsable'); + var uid = collapsable.parent.uid; + if (collapsed.indexOf(uid) === -1) { + collapsed.push(uid); + + console.log("Collapsing " + uid); + collapsable.parent.collapseAll(event); + } + }); + }, + destroy: function() {} + }; - if(opts.fx == 'slide') { - $box - .css({display: 'block'}) - .slideUp(opts.collapseDelay, function () { - if (typeof opts.onCollapsed == 'function') opts.onCollapsed.call($parent) - }); - } - else { - if(opts.fx == 'toggle') { - $box.hide(); - } - if(typeof opts.onCollapsed == 'function') { - t = setTimeout(function () { - opts.onCollapsed.call($parent); - }, opts.collapseDelay); - } - } - } + // Private variables + var collapsableUids = []; - var methods = { - init: function(options) { - var opts = $.extend(true, {}, $.fn.collapsable.defaults, options); - var $boxSet = $(this); - var $extLinks = opts.extLinks ? $(opts.extLinks) : $([]); - var spinnerExt = $.nette ? $.nette.ext('spinner') : null; - - var fragment = window.location.href; - if ((i = fragment.search(/#/)) != -1) - fragment = fragment.substring(i+1); - else - fragment = ''; - - if(opts.fx === 'slide' && ! opts.collapseDelay) { - opts.collapseDelay = 500; - } - if($extLinks.length) { - if ($extLinks[0].tagName.toUpperCase() != 'A') - $extLinks = $extLinks.find('a'); + // Private functions + function getCollapsableUid() { + var uid = undefined; + while (!uid) { + uid = 'Collapsable_' + Math.random(); + if (collapsableUids[uid]) { + uid = undefined; + } + } + collapsableUids.push(uid); - $extLinks.bind('click', function(e) { - var $target = $($(this).attr('href')); + return uid; + } - if (opts.extLinksPreventDefault) - e.preventDefault(); - if ($target.hasClass(opts.classNames.collapsed)) { - var $control = $target.find(opts.control); - var $btn = ($control[0].tagName.toUpperCase() == 'A') ? $control : $control.find('a'); + // $.collasable + $.fn.collapsable = function(options) { + if (methods[options]) { + return methods[options].apply(this, Array.prototype.slice.call(arguments, 1)); + } + else if (typeof options === 'object' || !options) { + var ret = methods.init.apply( this, arguments ); - $btn.trigger('click'); - } + var data = ret.data('collapsable'); + if (data && typeof data.parent.opts.onInit == 'function') { + $(this).each(function() { + data.parent.opts.onInit.call($(this)); }); } - if(opts.expandAll) - $(opts.expandAll).bind('click', function() { methods.expandAll.call($boxSet); return false; }); + return ret; + } + else { + $.error('Method ' + options + ' does not exist on jQuery.collapsable' ); + } + }; + + $.fn.collapsable.defaults = defaults; - if(opts.collapseAll) - $(opts.collapseAll).bind('click', function() { methods.collapseAll.call($boxSet); return false; }); + var Collapsable = function($boxSet, options) { + this.opts = $.extend(true, {}, $.fn.collapsable.defaults, options); + this.items = []; + this.$boxSet = $boxSet; + this.$extLinks = this.opts.extLinks.selector ? $(this.opts.extLinks.selector) : $([]); - if(opts.grouped && !opts.collapsableAll && (fragment === '' || $boxSet.filter('#' + fragment).length === 0)) { - var $visible = $boxSet.filter('.' + opts.classNames.defaultExpanded); - if($visible.length == 0) - $boxSet.first().addClass(opts.classNames.defaultExpanded); - } + var that = this; + var fragment = window.location.href; - if ((fragment && ($fragment = $boxSet.filter('#' + fragment)).length)) { - $boxSet.filter('.' + opts.classNames.defaultExpanded).removeClass(opts.classNames.defaultExpanded); - $fragment.addClass(opts.classNames.defaultExpanded); - } + this.uid = getCollapsableUid(); - return $boxSet.each(function() { - var $this = $(this); - var $control = $this.find(opts.control); - var $box = $this.find(opts.box); + // search for #hash in url + if ((i = fragment.search(/#/)) != -1) { + fragment = fragment.substring(i+1); + } else { + fragment = ''; + } - if($control.length == 0 || $box.length == 0) - return true; // return false by ukončil celý each!!! + // default fxDuration in case of slide function + if (this.opts.fx === 'slide' && ! this.opts.fxDuration) { + this.opts.fxDuration = 500; + } - if($control.find('a').length == 0 && $control[0].tagName.toUpperCase() != 'A') - $control.wrapInner(''); + if (this.$extLinks.length) { + // we assume same structure among all $extLinks! + if (this.$extLinks[0].tagName.toUpperCase() != 'A') + this.$extLinks = this.$extLinks.find('a'); - $btn = ($control[0].tagName.toUpperCase() == 'A') ? $control : $control.find('a'); + this.$extLinks.on('click', function(event) { + var $target = $($(this).attr('href')); - if(opts.defaultOpen || $this.hasClass(opts.classNames.defaultExpanded) || $this.attr('id') == fragment) { - $this.removeClass(opts.classNames.collapsed).addClass(opts.classNames.expanded); + if (that.opts.extLinks.preventDefault) + event.preventDefault(); - $extLinks.filter('[href=#' + $this.attr('id') + ']').addClass(opts.classNames.extLinkActive); + if ($target.hasClass(that.opts.classNames.collapsed)) { + var $control = $target.find(that.opts.control); + var $btn = ($control[0].tagName.toUpperCase() == 'A') ? $control : $control.find('a'); - if(typeof opts.onExpanded == 'function') - opts.onExpanded.call($this); - } - else { - if(opts.fx !== false) $box.hide(); - $this.removeClass(opts.classNames.expanded).addClass(opts.classNames.collapsed); + $btn.trigger('click'); } + }); + } - $this.find($btn).bind('click', function(e) { - var data = $this.data('collapsable'); - if (data) - opts = data.opts; + if (this.opts.grouped && ! this.opts.collapsableAll && (fragment === '' || this.$boxSet.filter('#' + fragment).length === 0)) { + var $visible = this.$boxSet.filter('.' + this.opts.classNames.defaultExpanded); + if ($visible.length == 0) + this.$boxSet.first().addClass(this.opts.classNames.defaultExpanded); + } - if($this.hasClass(opts.classNames.expanded) && (!opts.grouped || opts.collapsableAll)) { - $extLinks.filter('[href=#' + $this.attr('id') + ']').removeClass(opts.classNames.extLinkActive); + if ((fragment && ($fragment = this.$boxSet.filter('#' + fragment)).length)) { + this.$boxSet + .filter('.' + this.opts.classNames.defaultExpanded) + .removeClass(this.opts.classNames.defaultExpanded); + $fragment.addClass(this.opts.classNames.defaultExpanded); + } - $this.removeClass(opts.classNames.expanded).addClass(opts.classNames.collapsed); - collapseBox(opts, $box, $this); - } - else { - if(opts.grouped) { - $extLinks.removeClass(opts.classNames.extLinkActive); - - var $visible = $boxSet.filter('.' + opts.classNames.expanded); - $boxSet.removeClass(opts.classNames.expanded).addClass(opts.classNames.collapsed); - if($visible.length > 0) { - collapseBox(opts, $visible.find(opts.box), $boxSet.filter($visible)); - } - } + this.$boxSet.each(function() { + var collapsable = new CollapsableItem(that, this) - $extLinks.filter('[href=#' + $this.attr('id') + ']').addClass(opts.classNames.extLinkActive); + if (collapsable.$box.length && collapsable.$control.length) { + that.items.push(collapsable); + } + }); - $this.removeClass(opts.classNames.collapsed).addClass(opts.classNames.expanded); - expandBox(opts, $box, $this); - } + return this; + }; - if($(this).hasClass('ajax') && spinnerExt) { - $box.append(spinnerExt.$spinnerHtml); - } + /** + * @return {Array} indexes of expanded items in Collapsable.items + */ + Collapsable.prototype.findExpanded = 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; + }; - if(opts.preventDefault) - e.preventDefault(); - }); + /** + * Collapses all expanded items + */ + Collapsable.prototype.collapseAll = function(event) { + event = event || { type: 'collapsable.collapseAll' }; - $(this).data('collapsable', {opts: opts}); - }); - }, + var expandedItems = this.findExpanded(); + var l = expandedItems.length; - expandAll: function() { - var $boxSet = $(this); - var data; - if(data = $boxSet.data('collapsable')) { - var opts = data.opts; + for (var i = 0; i < l; i++) { + this.items[expandedItems[i]].collapse(event); + } + }; - $boxSet.removeClass(opts.classNames.collapsed).addClass(opts.classNames.expanded); - $boxSet.each(function() { - expandBox(opts, $(this).find(opts.box), $(this)); - }); + /** + * Expands all expanded items + */ + Collapsable.prototype.expandAll = function(event) { + event = event || { type: 'collapsable.expandAll' }; - if(opts.preventDefault) - return false; - else - $(document).scrollTop($boxSet.first().offset().top); + var l = this.items.length; + for (var i = 0; i < l; i++) { + if (! this.items[i].isExpanded()) { + this.items[i].expand(event); } - }, + } + }; - collapseAll: function() { - var $boxSet = $(this); - var data; - if(data = $boxSet.data('collapsable')) { - var opts = data.opts; - $boxSet.removeClass(opts.classNames.expanded).addClass(opts.classNames.collapsed); - $boxSet.each(function() { - collapseBox(opts, $(this).find(opts.box), $(this)); - }); + /***** CollapsableItem - one instance of collapsable element ****/ + var CollapsableItem = function(parent, collapsable) { + // public + this.parent = parent; + this.$collapsable = $(collapsable); + this.id = this.$collapsable.attr('id'); + this.$control = this.$collapsable.find(parent.opts.control); + this.$box = this.$collapsable.find(parent.opts.box); + + // private + var that = this; + var $anchor; // clickable element + var opts = this.parent.opts; // shortcut + var spinnerExt = $.nette ? $.nette.ext('spinner') : null; + + var event = { + type: 'collapsable.init' + }; + + if (! this.id) { + this.id = 'CollapsableItem-' + (new Date()).getTime(); + this.$collapsable.attr('id', this.id); + } - if(opts.preventDefault) - return false; - else - $(document).scrollTop($boxSet.first().offset().top); - } - }, - destroy: function() { - var $boxSet = $(this); - $boxSet.each(function() { - var data; - var $this = $(this); - if(data = $this.data('collapsable')) { - $this.find(data.opts.box).removeAttr('style'); + if (this.$control.length == 0 || this.$box.length == 0) + return; - var $control = $this.find(data.opts.control); - var $btn = ($control[0].tagName.toUpperCase() == 'A') ? $control : $control.find('a'); + if (this.$control.find('a').length === 0 && this.$control[0].tagName.toUpperCase() != 'A') { + this.$control.wrapInner(''); + } + + $anchor = (this.$control[0].tagName.toUpperCase() === 'A') ? this.$control : this.$control.find('a'); - $this.removeClass(data.opts.classNames.collapsed + ' ' + data.opts.classNames.expanded); + if (this.$collapsable.hasClass(opts.classNames.defaultExpanded)) { + this.expand(event); + } + else { + if (opts.fx !== false) this.$box.hide(); + this.$collapsable + .removeClass(opts.classNames.expanded) + .addClass(opts.classNames.collapsed); + } - $btn.unbind('click'); - $btn.filter('.ca-link').each(function() { // pouze odkazy vytvořené pluginem chceme odstranit - $(this).parent().html($(this).html()); - }); + this.$collapsable.on(opts.event, $anchor, function(event) { + if (opts.preventDefault) { + event.preventDefault(); + } - if(data.opts.extLinks) - $(data.opts.extLinks).unbind('click'); + if (that.isExpanded()) { + that.collapse(event); + } + else { + if (opts.grouped) { + var expandedItem = that.parent.findExpanded(); // grouped -> max one expanded item + var hasCollapsed; + if (expandedItem.length) { + hasCollapsed = that.parent.items[expandedItem[0]].collapse(event, true); + + if (hasCollapsed === false) { + return; + } + } + } - if(data.opts.expandAll) - $(data.opts.expandAll).unbind('click'); + var hasExpanded = that.expand(event); - if(data.opts.collapseAll) - $(data.opts.collapseAll).unbind('click'); + if (! hasExpanded && opts.grouped) { + that.parent.items[expandedItem[0]].expand(event, true); + } - $this.removeData('collapsable'); + if (hasExpanded !== false && $(this).hasClass('ajax') && spinnerExt && spinnerExt.$spinnerHtml) { + that.$box.append(spinnerExt.$spinnerHtml); } - }); - } + } + }); + + this.$collapsable.data('collapsable', that); }; - $.fn.collapsable = function(options) { - if(methods[options]) { - return methods[options].apply(this, Array.prototype.slice.call(arguments, 1)); + /** + * @param {Object} event Event passed to function + * @param {Boolean} force Forcing CollapsableItem to expand regardless on onExpand return value + * @return {Boolean} Returns if CollapsableItem has been expanded or not + */ + CollapsableItem.prototype.expand = function(event, force) { + var opts = this.parent.opts; + if(typeof opts.onExpand == 'function') { + var expand = opts.onExpand.call(this.$collapsable, event); + if (expand === false && ! force) + return false; } - else if(typeof options === 'object' || !options) { - var ret = methods.init.apply( this, arguments ); - var data = ret.data('collapsable'); - if(data && typeof data.opts.onInit == 'function') { - $(this).each(function() { - data.opts.onInit.call(this); - }) - } + this.parent.$extLinks + .filter('[href=#' + this.id + ']') + .addClass(opts.extLinks.activeClass); - return ret; + this.$collapsable + .removeClass(opts.classNames.collapsed) + .addClass(opts.classNames.expanded); + + if(opts.fx == 'slide') { + this.$box + .slideDown(opts.fxDuration, function() { + if(typeof opts.onExpanded == 'function') opts.onExpanded.call(this.$collapsable, event); + }) + .css({ display: 'block' }); } else { - $.error('Method ' + options + ' does not exist on jQuery.collapsable' ); + if(opts.fx == 'toggle') { + this.$box.show(); + } + + if(typeof opts.onExpanded == 'function') { + t = setTimeout(function () { + opts.onExpanded.call(this.$collapsable, event); + }, opts.fxDuration); + } } + + return true; }; - $.fn.collapsable.defaults = { - control: '.ca-control', // selektor ovladaciho prvku, muze se v ramci boxu i opakovat - box: '.ca-box', // selektor pro skryvanou/zobrazovanou cast - fx: false, // [ false (jen zmena class) / toggle (prepinani show/hide) / slide (animace vysky) ] - collapseDelay: 0, // doba trvání efektu, případně zpoždění volání onExpanded a onCollapsed funkcí; výchozí hodnota 500, pokud je fx === 'slide' + /** + * @param {Object} event Event passed to function + * @param {Boolean} force Forcing CollapsableItem to collapse regardless on onCollapse return value + * @return {Boolean} Returns if CollapsableItem has been collapsed or not + */ + CollapsableItem.prototype.collapse = function(event, force) { + var opts = this.parent.opts; - grouped: false, // pokud je true, pak bude otevreny vzdy jen jeden box ze skupiny - defaultOpen: false, // otevreny/zavreny po inicializaci - preventDefault: true, // po kliku na control zavolani / nezavolani preventDefault na event - collapsableAll: true, // lze zavrit vsechny polozky, ma vliv pouze pokud grouped==true + if (! opts.collapsableAll && this.parent.findExpanded().length === 1 && ! force) { + return false; + } - extLinks: false, // false nebo selektor externich odkazu (napr. prichycen horni lista, atp.) - extLinksPreventDefault: false, // true / false zda po kliku na externí odkaz má být zavoláno preventDefault na události click - expandAll: false, // false nebo selektor na odkaz, ktery otevre vsechny boxy - collapseAll: false, // false nebo selektor na odkaz, ktery zavre vsechny boxy + if(typeof opts.onCollapse == 'function') { + var collapse = opts.onCollapse.call(this.$collapsable, event); + if (collapse === false && ! force) { + return false; + } + } - classNames: { // nazvy trid, otevreny, zavreny box a box, ktery bude defaultne otevreny - expanded: 'ca-expanded', - collapsed: 'ca-collapsed', - defaultExpanded: 'ca-default-expanded', - extLinkActive: 'ca-ext-active' - }, + this.parent.$extLinks + .filter('[href=#' + this.id + ']') + .removeClass(opts.extLinks.activeClass); - onInit: null, // callback funkce volana ihned po inicializaci - onExpand: null, // callback volany pred samotny oteviranim - onExpanded: null, // callback zavolany po dokonceni otevirani, ma smysl jen pri fx: 'slide' s nenulovym casem - onCollapse: null, // analogicky viz vyse - onCollapsed: null + this.$collapsable + .removeClass(opts.classNames.expanded) + .addClass(opts.classNames.collapsed); + + if(opts.fx == 'slide') { + this.$box + .css({ display: 'block' }) + .slideUp(opts.fxDuration, function () { + if (typeof opts.onCollapsed == 'function') opts.onCollapsed.call(this.$collapsable, event); + }); + } + else { + if(opts.fx == 'toggle') { + this.$box.hide(); + } + + if(typeof opts.onCollapsed == 'function') { + t = setTimeout(function () { + opts.onCollapsed.call(this.$collapsable, event); + }, opts.fxDuration); + } + } + + return true; + }; + + CollapsableItem.prototype.isExpanded = function() { + return this.$collapsable.hasClass(this.parent.opts.classNames.expanded); }; })(jQuery); From 6f30b0bce6829677aeb06e0e6e620fe03671325f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20=C5=A0er=C3=BD?= Date: Wed, 24 Feb 2016 00:38:27 +0100 Subject: [PATCH 03/10] #0 v2.0.0 - wip --- jquery.collapsable.js | 386 +++++++++++++++++++++++++++--------------- 1 file changed, 251 insertions(+), 135 deletions(-) diff --git a/jquery.collapsable.js b/jquery.collapsable.js index 5bd3e64..8cacaa6 100644 --- a/jquery.collapsable.js +++ b/jquery.collapsable.js @@ -1,5 +1,18 @@ +/** + * jQuery plugin for collapsable boxes + * + * @copyright Copyright (c) 2014-2016 Radek Šerý + * @license MIT + * + * @version 2.0.0 + */ ;(function($) { + + /** + * Collapsable defaults + * @type {{control: string, box: string, event: string, fx: boolean, fxDuration: number, grouped: boolean, collapsableAll: boolean, preventDefault: boolean, extLinks: {selector: null, preventDefault: boolean, activeClass: string}, classNames: {expanded: string, collapsed: string, defaultExpanded: string}, onInit: null, onExpand: null, onExpanded: null, onCollapse: null, onCollapsed: null}} + */ var defaults = { control: '.ca-control', // selektor ovladaciho prvku, muze se v ramci boxu i opakovat box: '.ca-box', // selektor pro skryvanou/zobrazovanou cast @@ -14,6 +27,7 @@ extLinks: { selector: null, // false nebo selektor externich odkazu (napr. prichycen horni lista, atp.) + //openOnly: true @todo preventDefault: false, // true / false zda po kliku na externí odkaz má být zavoláno preventDefault na události click activeClass: 'ca-ext-active' }, @@ -26,134 +40,158 @@ onInit: null, // callback funkce volana ihned po inicializaci onExpand: null, // callback volany pred samotny oteviranim - onExpanded: null, // callback zavolany po dokonceni otevirani, ma smysl jen pri fx: 'slide' s nenulovym casem + onExpanded: null, // callback zavolany po dokonceni otevirani, ma smysl jen pri fxDuration > 0 onCollapse: null, // analogicky viz vyse onCollapsed: null }; - // public methods which can be used via .collapsable('method') + + /** + * Public methods available via jQuery adapter + * @type {{init: methods.init, expandAll: methods.expandAll, collapseAll: methods.collapseAll, destroy: methods.destroy}} + */ var methods = { init: function(options) { var instance = new Collapsable(this, options); return this; }, expandAll: function(event) { + handlePublicMethods.call(this, 'expandAll', event); }, collapseAll: function(event) { - var collapsed = []; - this.each(function() { - var collapsable = $(this).data('collapsable'); - var uid = collapsable.parent.uid; - if (collapsed.indexOf(uid) === -1) { - collapsed.push(uid); - - console.log("Collapsing " + uid); - collapsable.parent.collapseAll(event); - } - }); + handlePublicMethods.call(this, 'collapseAll', event); }, - destroy: function() {} + destroy: function(event) { + handlePublicMethods.call(this, 'destroy', event); + } }; - // Private variables - var collapsableUids = []; + /** + * Last used uid index + * @type {number} + */ + var caUid = 0; - // Private functions - function getCollapsableUid() { - var uid = undefined; - while (!uid) { - uid = 'Collapsable_' + Math.random(); - if (collapsableUids[uid]) { - uid = undefined; - } - } - collapsableUids.push(uid); + /** + * Generates unique id for new Collapsable object + * @returns {String} uid + * @private + */ + function getUid() { + return 'ca-uid-' + caUid++; + } + + + /** + * Handles public method called on jQuery object using adapter + * @param {String} action - 'collapseAll' | 'expandAll' + * @param {Object} event - event (object) passed by user + * @private + */ + function handlePublicMethods(action, event) { + 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); - return uid; + instance.parent[action](event); + } + } + }); } - // $.collasable - $.fn.collapsable = function(options) { - if (methods[options]) { - return methods[options].apply(this, Array.prototype.slice.call(arguments, 1)); + /** + * Finds ext links specified in options and bind click event to them for opening + * @this Collapsable + * @private + */ + function handleExtLinks() { + if ((this.$extLinks = $(this.opts.extLinks.selector).filter('a')).length) { + var that = this; + + this.$extLinks.on('click', function(event) { + if (that.opts.extLinks.preventDefault) + event.preventDefault(); + + var collapsable = $($(this).attr('href')).data('collapsable'); + + if (collapsable) { + collapsable.$control.find('a').trigger('click'); + } + }); } - else if (typeof options === 'object' || !options) { - var ret = methods.init.apply( this, arguments ); + } - var data = ret.data('collapsable'); - if (data && typeof data.parent.opts.onInit == 'function') { - $(this).each(function() { - data.parent.opts.onInit.call($(this)); - }); - } - return ret; + /** + * Prepare default expanded item. Checks the necessity of expanded item (collapsableAll set to false && grouped set + * to true) and limits amount of default expanded items to 1 when grouped 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. + * @this Collapsable + * @private + */ + function prepareDefaultExpanded() { + var fragment = window.location.href; + var i = fragment.search(/#/); + + // search for #hash in url + if (i !== -1) { + fragment = fragment.substring(i + 1); + var $fragment = this.$boxSet.filter('#' + fragment); + + if ($fragment.length) { + this.$boxSet + .filter('.' + this.opts.classNames.defaultExpanded) + .removeClass(this.opts.classNames.defaultExpanded); + $fragment.addClass(this.opts.classNames.defaultExpanded); + + return; + } } - else { - $.error('Method ' + options + ' does not exist on jQuery.collapsable' ); + + if (this.opts.grouped) { + var $visible = this.$boxSet.filter('.' + this.opts.classNames.defaultExpanded); + + if (! this.opts.collapsableAll && $fragment.length === 0 && $visible.length == 0) { + this.$boxSet.first().addClass(this.opts.classNames.defaultExpanded); + } + if ($visible.length > 1) { + $visible.slice(1).removeClass(this.opts.classNames.defaultExpanded); + } } - }; + } - $.fn.collapsable.defaults = defaults; + /** + * @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 = []; this.$boxSet = $boxSet; - this.$extLinks = this.opts.extLinks.selector ? $(this.opts.extLinks.selector) : $([]); var that = this; - var fragment = window.location.href; - this.uid = getCollapsableUid(); - - // search for #hash in url - if ((i = fragment.search(/#/)) != -1) { - fragment = fragment.substring(i+1); - } else { - fragment = ''; - } + this.uid = getUid(); // default fxDuration in case of slide function if (this.opts.fx === 'slide' && ! this.opts.fxDuration) { this.opts.fxDuration = 500; } - if (this.$extLinks.length) { - // we assume same structure among all $extLinks! - if (this.$extLinks[0].tagName.toUpperCase() != 'A') - this.$extLinks = this.$extLinks.find('a'); - - this.$extLinks.on('click', function(event) { - var $target = $($(this).attr('href')); - - if (that.opts.extLinks.preventDefault) - event.preventDefault(); - - if ($target.hasClass(that.opts.classNames.collapsed)) { - var $control = $target.find(that.opts.control); - var $btn = ($control[0].tagName.toUpperCase() == 'A') ? $control : $control.find('a'); + handleExtLinks.call(this); - $btn.trigger('click'); - } - }); - } - - if (this.opts.grouped && ! this.opts.collapsableAll && (fragment === '' || this.$boxSet.filter('#' + fragment).length === 0)) { - var $visible = this.$boxSet.filter('.' + this.opts.classNames.defaultExpanded); - if ($visible.length == 0) - this.$boxSet.first().addClass(this.opts.classNames.defaultExpanded); - } - - if ((fragment && ($fragment = this.$boxSet.filter('#' + fragment)).length)) { - this.$boxSet - .filter('.' + this.opts.classNames.defaultExpanded) - .removeClass(this.opts.classNames.defaultExpanded); - $fragment.addClass(this.opts.classNames.defaultExpanded); - } + prepareDefaultExpanded.call(this); this.$boxSet.each(function() { var collapsable = new CollapsableItem(that, this) @@ -167,25 +205,32 @@ }; /** - * @return {Array} indexes of expanded items in Collapsable.items + * Returns indexes of expanded items in Collapsable.items + * @returns {Array} */ - Collapsable.prototype.findExpanded = function() { + Collapsable.prototype.getExpanded = function() { var expanded = []; var l = this.items.length; + for (var i = 0; i < l; i++) { - if (this.items[i].isExpanded()) + if (this.items[i].isExpanded()) { expanded.push(i); + } } + return expanded; }; + + // @todo zamyslet se nad sjednocením collapseAll a expandAll, zamyslet se nad tím, kdy bránit zavření poslední položky při collapsableAll===false (a kterou nechat otevřenou) a kdy zabránit otevření všech položek při grouped===true /** * Collapses all expanded items + * @param {Event} event - event to be passed to onCollapse and onCollapsed callbacks */ Collapsable.prototype.collapseAll = function(event) { event = event || { type: 'collapsable.collapseAll' }; - var expandedItems = this.findExpanded(); + var expandedItems = this.getExpanded(); var l = expandedItems.length; for (var i = 0; i < l; i++) { @@ -193,66 +238,98 @@ } }; + /** * Expands all expanded items + * @param {Event} event - event to be passed to onExpand and onExpanded callbacks */ Collapsable.prototype.expandAll = function(event) { + // if grouped, we only want to expand one (first) box, or none if already expanded + if (this.opts.grouped && this.getExpanded().length) { + return; + } + event = event || { type: 'collapsable.expandAll' }; var l = this.items.length; + for (var i = 0; i < l; i++) { if (! this.items[i].isExpanded()) { - this.items[i].expand(event); + var expanded = this.items[i].expand(event); + + if (this.opts.grouped && expanded) { + break; + } } } }; - /***** CollapsableItem - one instance of collapsable element ****/ - var CollapsableItem = function(parent, collapsable) { - // public + /** + * Expand or collapse item based on if it should expanded by default. Called on initialization within the context + * of the item itself. + * @this CollapsableItem + * @private + */ + function handleDefaultExpanded() { + var event = { + type: 'collapsable.init' + }; + var opts = this.parent.opts; + + // 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'; + + if (this.$collapsable.hasClass(opts.classNames.defaultExpanded)) { + this.expand(event); + } else { + this.collapse(event); + } + + opts.fx = fx; + } + + + /** + * 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 = $(collapsable); + 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); - // private + + if (this.$control.length == 0 || this.$box.length == 0) + return; + var that = this; - var $anchor; // clickable element + var selector; // selector for clickable element var opts = this.parent.opts; // shortcut var spinnerExt = $.nette ? $.nette.ext('spinner') : null; - var event = { - type: 'collapsable.init' - }; - if (! this.id) { - this.id = 'CollapsableItem-' + (new Date()).getTime(); + this.id = getUid(); this.$collapsable.attr('id', this.id); } - - if (this.$control.length == 0 || this.$box.length == 0) - return; + handleDefaultExpanded.call(this); if (this.$control.find('a').length === 0 && this.$control[0].tagName.toUpperCase() != 'A') { this.$control.wrapInner(''); } - $anchor = (this.$control[0].tagName.toUpperCase() === 'A') ? this.$control : this.$control.find('a'); - - if (this.$collapsable.hasClass(opts.classNames.defaultExpanded)) { - this.expand(event); - } - else { - if (opts.fx !== false) this.$box.hide(); - this.$collapsable - .removeClass(opts.classNames.expanded) - .addClass(opts.classNames.collapsed); - } - - this.$collapsable.on(opts.event, $anchor, function(event) { + selector = (this.$control[0].tagName.toUpperCase() === 'A') ? opts.control : opts.control + ' a'; + this.$collapsable.on(opts.event, selector, function(event) { if (opts.preventDefault) { event.preventDefault(); } @@ -261,37 +338,39 @@ that.collapse(event); } else { + // @todo prověřit celou věc s force zavíráním v případě grouped elementů a návratové hodnotě onExpand false; plus chování při collapsableAll domyslet if (opts.grouped) { - var expandedItem = that.parent.findExpanded(); // grouped -> max one expanded item - var hasCollapsed; - if (expandedItem.length) { - hasCollapsed = that.parent.items[expandedItem[0]].collapse(event, true); - - if (hasCollapsed === false) { - return; - } + var expandedItem = that.parent.getExpanded(); // grouped -> max one expanded item + // if grouped element hasn't collapsed, we can't continue + if (expandedItem.length && that.parent.items[expandedItem[0]].collapse(event, true) === false) { + return; } } var hasExpanded = that.expand(event); - if (! hasExpanded && opts.grouped) { - that.parent.items[expandedItem[0]].expand(event, true); + // if box has not opened and collapsableAll is set to false, we must make sure something is opened + if (! hasExpanded && opts.grouped && ! opts.collapsableAll) { + that.parent.items[expandedItem[0]].expand(event, true); // collapsableAll === true, so expandedItem cannot be empty } - if (hasExpanded !== false && $(this).hasClass('ajax') && spinnerExt && spinnerExt.$spinnerHtml) { + if (hasExpanded && $(this).hasClass('ajax') && spinnerExt && spinnerExt.$spinnerHtml) { that.$box.append(spinnerExt.$spinnerHtml); } } }); this.$collapsable.data('collapsable', that); + + return this; }; + + // @todo přepsat expand a collapse funkce do jedné? /** - * @param {Object} event Event passed to function - * @param {Boolean} force Forcing CollapsableItem to expand regardless on onExpand return value - * @return {Boolean} Returns if CollapsableItem has been expanded or not + * @param {Object} event - event passed to function + * @param {boolean} force - forcing CollapsableItem to expand regardless on onExpand return value + * @returns {boolean} - returns if CollapsableItem has been expanded or not */ CollapsableItem.prototype.expand = function(event, force) { var opts = this.parent.opts; @@ -312,6 +391,7 @@ if(opts.fx == 'slide') { this.$box .slideDown(opts.fxDuration, function() { + // @todo vadné this if(typeof opts.onExpanded == 'function') opts.onExpanded.call(this.$collapsable, event); }) .css({ display: 'block' }); @@ -323,6 +403,7 @@ if(typeof opts.onExpanded == 'function') { t = setTimeout(function () { + // @todo vadné this opts.onExpanded.call(this.$collapsable, event); }, opts.fxDuration); } @@ -332,14 +413,14 @@ }; /** - * @param {Object} event Event passed to function - * @param {Boolean} force Forcing CollapsableItem to collapse regardless on onCollapse return value - * @return {Boolean} Returns if CollapsableItem has been collapsed or not + * @param {Object} event - Event passed to function + * @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(event, force) { var opts = this.parent.opts; - if (! opts.collapsableAll && this.parent.findExpanded().length === 1 && ! force) { + if (! opts.collapsableAll && this.parent.getExpanded().length === 1 && ! force) { return false; } @@ -362,6 +443,7 @@ this.$box .css({ display: 'block' }) .slideUp(opts.fxDuration, function () { + // @todo vadné this if (typeof opts.onCollapsed == 'function') opts.onCollapsed.call(this.$collapsable, event); }); } @@ -372,6 +454,7 @@ if(typeof opts.onCollapsed == 'function') { t = setTimeout(function () { + // @todo vadné this opts.onCollapsed.call(this.$collapsable, event); }, opts.fxDuration); } @@ -380,8 +463,41 @@ return true; }; + /** + * Tests if the element is expanded or not + * @returns {Boolean} + */ CollapsableItem.prototype.isExpanded = function() { return this.$collapsable.hasClass(this.parent.opts.classNames.expanded); }; + + /** + * jQuery adapter for Collapsable object, returns elements on which it was called, chainable + * @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) { + var ret = methods.init.apply(this, arguments); + + var data = ret.data('collapsable'); + if (data && typeof data.parent.opts.onInit === 'function') { + $(this).each(function() { + data.parent.opts.onInit.call($(this)); + }); + } + + return ret; + } + else { + $.error('Method ' + options + ' does not exist on jQuery.collapsable'); + } + }; + + $.fn.collapsable.defaults = defaults; + })(jQuery); From aec6f5dcba3f9b2956b411577a89be3027920f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20=C5=A0er=C3=BD?= Date: Thu, 25 Feb 2016 23:43:35 +0100 Subject: [PATCH 04/10] #0 v2.0.0 - almost there! - wip --- jquery.collapsable.js | 303 +++++++++++++++++++++++------------------- 1 file changed, 169 insertions(+), 134 deletions(-) diff --git a/jquery.collapsable.js b/jquery.collapsable.js index 8cacaa6..3aac34d 100644 --- a/jquery.collapsable.js +++ b/jquery.collapsable.js @@ -1,48 +1,51 @@ /** * jQuery plugin for collapsable boxes * + * @author Radek Šerý * @copyright Copyright (c) 2014-2016 Radek Šerý * @license MIT * * @version 2.0.0 + * + * @todo add aria support */ ;(function($) { - /** * Collapsable defaults * @type {{control: string, box: string, event: string, fx: boolean, fxDuration: number, grouped: boolean, collapsableAll: boolean, preventDefault: boolean, extLinks: {selector: null, preventDefault: boolean, activeClass: string}, classNames: {expanded: string, collapsed: string, defaultExpanded: string}, onInit: null, onExpand: null, onExpanded: null, onCollapse: null, onCollapsed: null}} */ var defaults = { - control: '.ca-control', // selektor ovladaciho prvku, muze se v ramci boxu i opakovat - box: '.ca-box', // selektor pro skryvanou/zobrazovanou cast - event: 'click', // událost, na kterou je u ca-control bindované otevírání - - fx: false, // [ false (jen zmena class) / toggle (prepinani show/hide) / slide (animace vysky) ] - fxDuration: 0, // doba trvání efektu, případně zpoždění volání onExpanded a onCollapsed funkcí; výchozí hodnota 500, pokud je fx === 'slide' - - grouped: false, // pokud je true, pak bude otevreny vzdy jen jeden box ze skupiny - collapsableAll: true, // lze zavrit vsechny polozky, ma vliv pouze pokud grouped==true - preventDefault: true, // po kliku na control zavolani / nezavolani preventDefault na event - - extLinks: { - selector: null, // false nebo selektor externich odkazu (napr. prichycen horni lista, atp.) - //openOnly: true @todo - preventDefault: false, // true / false zda po kliku na externí odkaz má být zavoláno preventDefault na události click - activeClass: 'ca-ext-active' + control: '.ca-control', // CSS selector for control element + box: '.ca-box', // CSS selector for hideable element (box) + event: 'click', // event triggering the expand/collapse + + fx: false, // effect for expanding/collapsing, [ false | toggle | slide ] + fxDuration: 0, // duration of the effect, affects delay between onExpand (onCollapse) and onExpanded (onCollapsed) callbacks; default value is 500 when fx set to slide + + grouped: 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 + preventDefault: true, // whether prevenDefault should be called when specified event occurs on control + + extLinks: { // external links for operating collapsable set, can be anywhere else in DOM + selector: null, // CSS selector for external links; it has to be anchors; the click event is binded + //openOnly: true // @todo create possibility for extLinks would only open the boxes? + preventDefault: false, // whether preventDefault is called on extLinks click + activeClass: 'ca-ext-active' // class which would be toggled on external link when associated box is expanded or collapsed }, - classNames: { // nazvy trid, otevreny, zavreny box a box, ktery bude defaultne otevreny + 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' }, - onInit: null, // callback funkce volana ihned po inicializaci - onExpand: null, // callback volany pred samotny oteviranim - onExpanded: null, // callback zavolany po dokonceni otevirani, ma smysl jen pri fxDuration > 0 - onCollapse: null, // analogicky viz vyse - onCollapsed: null + // callbacks + onInit: null, // immediately after initialization + onExpand: null, // called when box expansion starts + onExpanded: null, // called when box expansion ends + onCollapse: null, // called when box collapsion starts + onCollapsed: null // called when box collapsion ends }; @@ -52,8 +55,7 @@ */ var methods = { init: function(options) { - var instance = new Collapsable(this, options); - return this; + return new Collapsable(this, options); }, expandAll: function(event) { handlePublicMethods.call(this, 'expandAll', event); @@ -62,7 +64,7 @@ handlePublicMethods.call(this, 'collapseAll', event); }, destroy: function(event) { - handlePublicMethods.call(this, 'destroy', event); + handlePublicMethods.call(this, 'destroy', event); // @todo destroy :) } }; @@ -70,6 +72,7 @@ /** * Last used uid index * @type {number} + * @private */ var caUid = 0; @@ -86,7 +89,7 @@ /** * Handles public method called on jQuery object using adapter - * @param {String} action - 'collapseAll' | 'expandAll' + * @param {String} action - collapseAll|expandAll|destroy @todo destroy * @param {Object} event - event (object) passed by user * @private */ @@ -123,7 +126,7 @@ var collapsable = $($(this).attr('href')).data('collapsable'); if (collapsable) { - collapsable.$control.find('a').trigger('click'); + collapsable.$control.find('a').trigger('click', event); } }); } @@ -134,6 +137,8 @@ * Prepare default expanded item. Checks the necessity of expanded item (collapsableAll set to false && grouped set * to true) and limits amount of default expanded items to 1 when grouped 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 */ @@ -141,31 +146,84 @@ 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); - var $fragment = this.$boxSet.filter('#' + fragment); + defaultExpandedFromUrl = $items.index($('#' + fragment)); - if ($fragment.length) { - this.$boxSet - .filter('.' + this.opts.classNames.defaultExpanded) - .removeClass(this.opts.classNames.defaultExpanded); - $fragment.addClass(this.opts.classNames.defaultExpanded); + if (defaultExpandedFromUrl !== -1) { + this.items[defaultExpandedFromUrl].defaultExpanded = true; - return; + // max 1, we can return now + if (this.opts.grouped) { + return; + } } } + // max 1 expanded item if (this.opts.grouped) { - var $visible = this.$boxSet.filter('.' + this.opts.classNames.defaultExpanded); + defaultExpanded = $items.index($('.' + this.opts.classNames.defaultExpanded)); - if (! this.opts.collapsableAll && $fragment.length === 0 && $visible.length == 0) { - this.$boxSet.first().addClass(this.opts.classNames.defaultExpanded); + // max 1, we can return now + if (defaultExpanded !== -1) { + this.items[defaultExpanded].defaultExpanded = true; + return; } - if ($visible.length > 1) { - $visible.slice(1).removeClass(this.opts.classNames.defaultExpanded); + } + + // not grouped, 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 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 + * @this Collapsable + * @private + */ + function handleDefaultExpanded() { + var event = { type: 'collapsable.init' }; + var opts = this.opts; // shortcut + 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, @todo 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? + for (var i = 0; i < l; i++) { + if (items[i].defaultExpanded) { + items[i].expand(event, force); + force = false; + } else { + items[i].collapse(event, true); // on init, we want to close all regardless - if you return false, than we shouln't have the class set in first place } } + + opts.fx = fx; } @@ -178,11 +236,11 @@ var Collapsable = function($boxSet, options) { this.opts = $.extend(true, {}, $.fn.collapsable.defaults, options); this.items = []; - this.$boxSet = $boxSet; var that = this; this.uid = getUid(); + this.promiseOpen = false; // default fxDuration in case of slide function if (this.opts.fx === 'slide' && ! this.opts.fxDuration) { @@ -191,16 +249,17 @@ handleExtLinks.call(this); - prepareDefaultExpanded.call(this); - - this.$boxSet.each(function() { - var collapsable = new CollapsableItem(that, 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; }; @@ -222,25 +281,8 @@ }; - // @todo zamyslet se nad sjednocením collapseAll a expandAll, zamyslet se nad tím, kdy bránit zavření poslední položky při collapsableAll===false (a kterou nechat otevřenou) a kdy zabránit otevření všech položek při grouped===true - /** - * Collapses all expanded items - * @param {Event} event - event to be passed to onCollapse and onCollapsed callbacks - */ - Collapsable.prototype.collapseAll = function(event) { - event = event || { type: 'collapsable.collapseAll' }; - - var expandedItems = this.getExpanded(); - var l = expandedItems.length; - - for (var i = 0; i < l; i++) { - this.items[expandedItems[i]].collapse(event); - } - }; - - /** - * Expands all expanded items + * Expands all collapsed items * @param {Event} event - event to be passed to onExpand and onExpanded callbacks */ Collapsable.prototype.expandAll = function(event) { @@ -266,32 +308,19 @@ /** - * Expand or collapse item based on if it should expanded by default. Called on initialization within the context - * of the item itself. - * @this CollapsableItem - * @private + * Collapses all expanded items + * @param {Event} event - event to be passed to onCollapse and onCollapsed callbacks */ - function handleDefaultExpanded() { - var event = { - type: 'collapsable.init' - }; - var opts = this.parent.opts; - - // save fx so it can be set back - var fx = opts.fx; + Collapsable.prototype.collapseAll = function(event) { + event = event || { type: 'collapsable.collapseAll' }; - // for initialization, we don't want to use any effect - if (opts.fx) - opts.fx = 'toggle'; + var expandedItems = this.getExpanded(); + var l = expandedItems.length; - if (this.$collapsable.hasClass(opts.classNames.defaultExpanded)) { - this.expand(event); - } else { - this.collapse(event); + for (var i = 0; i < l; i++) { + this.items[expandedItems[i]].collapse(event); } - - opts.fx = fx; - } + }; /** @@ -308,59 +337,42 @@ 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 selector; // selector for clickable element var opts = this.parent.opts; // shortcut - var spinnerExt = $.nette ? $.nette.ext('spinner') : null; if (! this.id) { this.id = getUid(); this.$collapsable.attr('id', this.id); } - handleDefaultExpanded.call(this); - + // @todo umožnit různou strukturu ca-control, nyní musí být vždy stejná, tj. nelze

... a

...

v jednom boxu - nevytvoří se odkaz ve druhém elementu + // souvisí s tím i hodnota proměnné selector níže if (this.$control.find('a').length === 0 && this.$control[0].tagName.toUpperCase() != 'A') { this.$control.wrapInner(''); } selector = (this.$control[0].tagName.toUpperCase() === 'A') ? opts.control : opts.control + ' a'; - this.$collapsable.on(opts.event, selector, function(event) { + + // data contains arguments passed when trigger is called, used for passing event that triggered opening (eg. extLink click) + this.$collapsable.on(opts.event, selector, function(event, originalEvent) { + var passEvent = originalEvent ? originalEvent : event; if (opts.preventDefault) { event.preventDefault(); } if (that.isExpanded()) { - that.collapse(event); + that.collapse(passEvent); } else { - // @todo prověřit celou věc s force zavíráním v případě grouped elementů a návratové hodnotě onExpand false; plus chování při collapsableAll domyslet - if (opts.grouped) { - var expandedItem = that.parent.getExpanded(); // grouped -> max one expanded item - // if grouped element hasn't collapsed, we can't continue - if (expandedItem.length && that.parent.items[expandedItem[0]].collapse(event, true) === false) { - return; - } - } - - var hasExpanded = that.expand(event); - - // if box has not opened and collapsableAll is set to false, we must make sure something is opened - if (! hasExpanded && opts.grouped && ! opts.collapsableAll) { - that.parent.items[expandedItem[0]].expand(event, true); // collapsableAll === true, so expandedItem cannot be empty - } - - if (hasExpanded && $(this).hasClass('ajax') && spinnerExt && spinnerExt.$spinnerHtml) { - that.$box.append(spinnerExt.$spinnerHtml); - } + that.expand(passEvent); } }); - this.$collapsable.data('collapsable', that); + this.$collapsable.data('collapsable', this); return this; }; @@ -368,16 +380,39 @@ // @todo přepsat expand a collapse funkce do jedné? /** + * Expands single CollapsableItem; could be prevented by returning false from onExpand callback * @param {Object} event - event passed to function - * @param {boolean} force - forcing CollapsableItem to expand regardless on onExpand return value - * @returns {boolean} - returns if CollapsableItem has been expanded or not + * @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(event, force) { var opts = this.parent.opts; + var expandedItem = this.parent.getExpanded(); // grouped -> max one expanded item + + this.parent.promiseOpen = true; // allows us to collapse expanded item even if there might be collapseAll === false option + + if (opts.grouped) { + // before expanding, we have to collapse previously opened item + + // if grouped element hasn't collapsed, we can't continue + if (expandedItem.length && this.parent.items[expandedItem[0]].collapse(event, force) === false) { + this.parent.promiseOpen = false; + return false; + } + } + + this.parent.promiseOpen = false; + if(typeof opts.onExpand == 'function') { var expand = opts.onExpand.call(this.$collapsable, event); - if (expand === false && ! force) + if (expand === false && ! force) { + // collapsableAll === false && grouped === true, so if box has not opened, we must make sure something is opened, therefore we force-open previously opened box (simulating it has never closed in first place); if grouped + if (! opts.collapsableAll && opts.grouped) { + this.parent.items[expandedItem[0]].expand(event, true); + } + return false; + } } this.parent.$extLinks @@ -388,11 +423,11 @@ .removeClass(opts.classNames.collapsed) .addClass(opts.classNames.expanded); + var that = this; if(opts.fx == 'slide') { this.$box .slideDown(opts.fxDuration, function() { - // @todo vadné this - if(typeof opts.onExpanded == 'function') opts.onExpanded.call(this.$collapsable, event); + if(typeof opts.onExpanded == 'function') opts.onExpanded.call(that.$collapsable, event); }) .css({ display: 'block' }); } @@ -403,8 +438,7 @@ if(typeof opts.onExpanded == 'function') { t = setTimeout(function () { - // @todo vadné this - opts.onExpanded.call(this.$collapsable, event); + opts.onExpanded.call(that.$collapsable, event); }, opts.fxDuration); } } @@ -413,14 +447,16 @@ }; /** + * Collapses single CollapsableItem; could be prevented by returning false from onCollapse callback * @param {Object} event - Event passed to function - * @param {boolean} force - Forcing CollapsableItem to collapse regardless on onCollapse return value - * @returns {boolean} - Returns if CollapsableItem has been collapsed or not + * @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(event, force) { var opts = this.parent.opts; - if (! opts.collapsableAll && this.parent.getExpanded().length === 1 && ! force) { + // 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; } @@ -439,12 +475,12 @@ .removeClass(opts.classNames.expanded) .addClass(opts.classNames.collapsed); + var that = this; if(opts.fx == 'slide') { this.$box .css({ display: 'block' }) .slideUp(opts.fxDuration, function () { - // @todo vadné this - if (typeof opts.onCollapsed == 'function') opts.onCollapsed.call(this.$collapsable, event); + if (typeof opts.onCollapsed == 'function') opts.onCollapsed.call(that.$collapsable, event); }); } else { @@ -454,8 +490,7 @@ if(typeof opts.onCollapsed == 'function') { t = setTimeout(function () { - // @todo vadné this - opts.onCollapsed.call(this.$collapsable, event); + opts.onCollapsed.call(that.$collapsable, event); }, opts.fxDuration); } } @@ -464,7 +499,7 @@ }; /** - * Tests if the element is expanded or not + * Tests if the CollapsableItem is expanded or not * @returns {Boolean} */ CollapsableItem.prototype.isExpanded = function() { @@ -473,7 +508,7 @@ /** - * jQuery adapter for Collapsable object, returns elements on which it was called, chainable + * jQuery adapter for Collapsable object, returns elements on which it was called, so it's chainable * @param {Object} options - options to override plugin defaults * @returns {jQuery} */ @@ -482,16 +517,16 @@ return methods[options].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof options === 'object' || !options) { - var ret = methods.init.apply(this, arguments); + var data = methods.init.apply(this, arguments); - var data = ret.data('collapsable'); - if (data && typeof data.parent.opts.onInit === 'function') { - $(this).each(function() { - data.parent.opts.onInit.call($(this)); - }); + if (data && typeof data.opts.onInit === 'function') { + var l = data.items.length; + for (var i = 0; i < l; i++) { + data.opts.onInit.call(data.items[i].$collapsable); + } } - return ret; + return this; } else { $.error('Method ' + options + ' does not exist on jQuery.collapsable'); From 29c746b1d7bf43f8bd6946d5dce546eceb636e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20=C5=A0er=C3=BD?= Date: Fri, 26 Feb 2016 23:22:06 +0100 Subject: [PATCH 05/10] #0 v2.0.0 - getting real close ;) - wip - event wtf --- jquery.collapsable.js | 247 +++++++++++++++++++++++------------------- 1 file changed, 137 insertions(+), 110 deletions(-) diff --git a/jquery.collapsable.js b/jquery.collapsable.js index 3aac34d..8172e19 100644 --- a/jquery.collapsable.js +++ b/jquery.collapsable.js @@ -7,29 +7,35 @@ * * @version 2.0.0 * - * @todo add aria support */ ;(function($) { + // @todo: aria data atributy, viz http://heydonworks.com/practical_aria_examples/ + // @todo: dodělat vlastní eventy, init.collapsable, collapseAll/expandAll.collapsable, destroy.collapsable - může se hodit obnovení this.$boxSet, protože to chceme volat nad ním + // @todo: u expandAll.collapsable vymyslet preventDefault - měl by zabránit otevření všech boxů? analogicky i pro ostatní + // @todo: zamyslet se nad tím, co způsobí nahrazení callbacků za eventy v případě ajaxu, kdy se collapsable nahradí + + // @feature: díky předávání originalEvent do expand.collapsable (atd.) je možné použít e.originalEvent.preventDefault() místo defaults.preventDefault! cool, ne?! + /** * Collapsable defaults - * @type {{control: string, box: string, event: string, fx: boolean, fxDuration: number, grouped: boolean, collapsableAll: boolean, preventDefault: boolean, extLinks: {selector: null, preventDefault: boolean, activeClass: string}, classNames: {expanded: string, collapsed: string, defaultExpanded: string}, onInit: null, onExpand: null, onExpanded: null, onCollapse: null, onCollapsed: null}} + * @type {{control: string, box: string, event: string, fx: boolean, fxDuration: number, grouped: 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 - fx: false, // effect for expanding/collapsing, [ false | toggle | slide ] + fx: false, // effect for expanding/collapsing, [ false | toggle | slide | fade | {Object} ] fxDuration: 0, // duration of the effect, affects delay between onExpand (onCollapse) and onExpanded (onCollapsed) callbacks; default value is 500 when fx set to slide grouped: 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 - preventDefault: true, // whether prevenDefault should be called when specified event occurs on control + preventDefault: true, // whether prevenDefault should be called when specified event occurs on control; even if false, you may use e.originalEvent.preventDefault() inside collapsable event handlers extLinks: { // external links for operating collapsable set, can be anywhere else in DOM selector: null, // CSS selector for external links; it has to be anchors; the click event is binded - //openOnly: true // @todo create possibility for extLinks would only open the boxes? + //openOnly: true // @todo create possibility for extLinks would only open the boxes? for now, you might achieve this using callbacks and returning false preventDefault: false, // whether preventDefault is called on extLinks click activeClass: 'ca-ext-active' // class which would be toggled on external link when associated box is expanded or collapsed }, @@ -38,14 +44,9 @@ expanded: 'ca-expanded', collapsed: 'ca-collapsed', defaultExpanded: 'ca-default-expanded' - }, + } - // callbacks - onInit: null, // immediately after initialization - onExpand: null, // called when box expansion starts - onExpanded: null, // called when box expansion ends - onCollapse: null, // called when box collapsion starts - onCollapsed: null // called when box collapsion ends + // callbacks are no more available, use events instead }; @@ -79,7 +80,7 @@ /** * Generates unique id for new Collapsable object - * @returns {String} uid + * @returns {String} New uid * @private */ function getUid() { @@ -89,8 +90,8 @@ /** * Handles public method called on jQuery object using adapter - * @param {String} action - collapseAll|expandAll|destroy @todo destroy - * @param {Object} event - event (object) passed by user + * @param {String} action - CollapseAll|expandAll|destroy @todo destroy + * @param {Object} event - Event (object) passed by user * @private */ function handlePublicMethods(action, event) { @@ -201,8 +202,8 @@ * @private */ function handleDefaultExpanded() { - var event = { type: 'collapsable.init' }; - var opts = this.opts; // shortcut + var event; + var opts = this.opts; var items = this.items; // save fx so it can be set back @@ -215,6 +216,7 @@ var l = items.length; var force = ! opts.collapsableAll; // if we can't collapse all, we force expanding the first one chosen in prepareDefaultExpanded, @todo 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? for (var i = 0; i < l; i++) { + event = $.Event('init.collapsable', items[i].$collapsable); // @todo: dává toto smysl? neměla by event být společná? ukládá se opravdu do originalEvent? if (items[i].defaultExpanded) { items[i].expand(event, force); force = false; @@ -228,8 +230,39 @@ /** - * @param {jQuery} $boxSet - set of object to be initilized - * @param {Object} options - see plugin defaults + * 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 = { + expand: 'slideDown', + collapse: 'slideUp' + }; + } else if (this.opts.fx === 'fade') { + opts.fx = { + expand: 'fadeIn', + collapse: 'fadeOut' + }; + } + + if (opts.fx === 'slide' || opts.fx === 'fade') { + opts.fxDuration = opts.fxDuration || 500; + } + } + + + /** + * 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 */ @@ -242,13 +275,10 @@ this.uid = getUid(); this.promiseOpen = false; - // default fxDuration in case of slide function - if (this.opts.fx === 'slide' && ! this.opts.fxDuration) { - this.opts.fxDuration = 500; - } - handleExtLinks.call(this); + prepareFxOpt.call(this); + $boxSet.each(function() { var collapsable = new CollapsableItem(that, this); @@ -283,7 +313,7 @@ /** * Expands all collapsed items - * @param {Event} event - event to be passed to onExpand and onExpanded callbacks + * @param {Event} event - Event to be passed to onExpand and onExpanded callbacks */ Collapsable.prototype.expandAll = function(event) { // if grouped, we only want to expand one (first) box, or none if already expanded @@ -309,7 +339,7 @@ /** * Collapses all expanded items - * @param {Event} event - event to be passed to onCollapse and onCollapsed callbacks + * @param {Event} event - Event to be passed to onCollapse and onCollapsed callbacks */ Collapsable.prototype.collapseAll = function(event) { event = event || { type: 'collapsable.collapseAll' }; @@ -325,8 +355,8 @@ /** * 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 + * @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 */ @@ -378,124 +408,112 @@ }; - // @todo přepsat expand a collapse funkce do jedné? + /** + * + * @param {String} action - Either `expand` or `collapse` + * @param {Event} event - Event to be passed to callbacks + * @returns {boolean} + */ + function handleExpandCollapse(action, event) { + 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; + } + + // update extLinks + this.parent.$extLinks + .filter('[href=#' + this.id + ']') + [action === 'expand' ? 'addClass' : 'removeClass'](opts.extLinks.activeClass); + + // update classes on collapsable element itself + this.$collapsable + .removeClass(removeClass) + .addClass(addClass); + + // actually toggle the box state + if(typeof opts.fx === 'object') { + this.$box[opts.fx[action]](opts.fxDuration, function () { + that.$collapsable.trigger(trigger + '.collapsable'); + }); + } + else { + if(opts.fx == 'toggle') { + this.$box[action === 'expand' ? 'show' : 'hide'](); + } + + var t = setTimeout(function () { + that.$collapsable.trigger(trigger + '.collapsable'); + }, opts.fxDuration); + } + + return true; + } + /** * Expands single CollapsableItem; could be prevented by returning false from onExpand callback - * @param {Object} event - event passed to function - * @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 + * @param {Object} originalEvent - Event passed to function + * @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(event, force) { + CollapsableItem.prototype.expand = function(originalEvent, force) { var opts = this.parent.opts; var expandedItem = this.parent.getExpanded(); // grouped -> max one expanded item this.parent.promiseOpen = true; // allows us to collapse expanded item even if there might be collapseAll === false option - if (opts.grouped) { // before expanding, we have to collapse previously opened item - // if grouped element hasn't collapsed, we can't continue - if (expandedItem.length && this.parent.items[expandedItem[0]].collapse(event, force) === false) { + if (expandedItem.length && this.parent.items[expandedItem[0]].collapse(originalEvent, force) === false) { this.parent.promiseOpen = false; return false; } } - this.parent.promiseOpen = false; - if(typeof opts.onExpand == 'function') { - var expand = opts.onExpand.call(this.$collapsable, event); - if (expand === false && ! force) { - // collapsableAll === false && grouped === true, so if box has not opened, we must make sure something is opened, therefore we force-open previously opened box (simulating it has never closed in first place); if grouped - if (! opts.collapsableAll && opts.grouped) { - this.parent.items[expandedItem[0]].expand(event, true); - } + var event = $.Event('expand.collapsable', { originalEvent: originalEvent }); + this.$collapsable.trigger(event); - return false; + if (event.isDefaultPrevented() && ! force) { + // collapsableAll === false && grouped === true -> if the box has not opened, we must make sure something is opened, therefore we force-open previously opened box (opts.grouped is true means we tried to collapse something), simulating it has never closed in first place + if (! opts.collapsableAll && opts.grouped) { + this.parent.items[expandedItem[0]].expand(originalEvent, true); } - } - - this.parent.$extLinks - .filter('[href=#' + this.id + ']') - .addClass(opts.extLinks.activeClass); - - this.$collapsable - .removeClass(opts.classNames.collapsed) - .addClass(opts.classNames.expanded); - var that = this; - if(opts.fx == 'slide') { - this.$box - .slideDown(opts.fxDuration, function() { - if(typeof opts.onExpanded == 'function') opts.onExpanded.call(that.$collapsable, event); - }) - .css({ display: 'block' }); - } - else { - if(opts.fx == 'toggle') { - this.$box.show(); - } - - if(typeof opts.onExpanded == 'function') { - t = setTimeout(function () { - opts.onExpanded.call(that.$collapsable, event); - }, opts.fxDuration); - } + return false; } - return true; + return handleExpandCollapse.call(this, 'expand', originalEvent) }; /** * Collapses single CollapsableItem; could be prevented by returning false from onCollapse callback - * @param {Object} event - Event passed to function + * @param {Object} originalEvent - Event passed to function * @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(event, force) { + CollapsableItem.prototype.collapse = function(originalEvent, 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; } - if(typeof opts.onCollapse == 'function') { - var collapse = opts.onCollapse.call(this.$collapsable, event); - if (collapse === false && ! force) { - return false; - } - } - - this.parent.$extLinks - .filter('[href=#' + this.id + ']') - .removeClass(opts.extLinks.activeClass); + var event = $.Event('collapse.collapsable', { originalEvent: originalEvent }); + this.$collapsable.trigger(event); - this.$collapsable - .removeClass(opts.classNames.expanded) - .addClass(opts.classNames.collapsed); - - var that = this; - if(opts.fx == 'slide') { - this.$box - .css({ display: 'block' }) - .slideUp(opts.fxDuration, function () { - if (typeof opts.onCollapsed == 'function') opts.onCollapsed.call(that.$collapsable, event); - }); - } - else { - if(opts.fx == 'toggle') { - this.$box.hide(); - } - - if(typeof opts.onCollapsed == 'function') { - t = setTimeout(function () { - opts.onCollapsed.call(that.$collapsable, event); - }, opts.fxDuration); - } + if (event.isDefaultPrevented() && !force) { + return false; } - return true; + return handleExpandCollapse.call(this, 'collapse', originalEvent) }; /** @@ -507,9 +525,17 @@ }; + /** + * 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 - * @param {Object} options - options to override plugin defaults + * @function external:"jQuery.fn".collapsable + * @param {Object} options - Options to override plugin defaults * @returns {jQuery} */ $.fn.collapsable = function(options) { @@ -519,6 +545,7 @@ else if (typeof options === 'object' || !options) { var data = methods.init.apply(this, arguments); + // @todo: místo tohoto bude init.collapsable nad všemi položkami dohromady? if (data && typeof data.opts.onInit === 'function') { var l = data.items.length; for (var i = 0; i < l; i++) { From a2122d604604e8ee88ed7aa2ab66dbff2ad7b9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20=C5=A0er=C3=BD?= Date: Sat, 27 Feb 2016 14:37:42 +0100 Subject: [PATCH 06/10] #0 v2.0.0 - wip --- jquery.collapsable.js | 82 +++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/jquery.collapsable.js b/jquery.collapsable.js index 8172e19..5c11216 100644 --- a/jquery.collapsable.js +++ b/jquery.collapsable.js @@ -10,10 +10,9 @@ */ ;(function($) { - // @todo: aria data atributy, viz http://heydonworks.com/practical_aria_examples/ - // @todo: dodělat vlastní eventy, init.collapsable, collapseAll/expandAll.collapsable, destroy.collapsable - může se hodit obnovení this.$boxSet, protože to chceme volat nad ním - // @todo: u expandAll.collapsable vymyslet preventDefault - měl by zabránit otevření všech boxů? analogicky i pro ostatní - // @todo: zamyslet se nad tím, co způsobí nahrazení callbacků za eventy v případě ajaxu, kdy se collapsable nahradí + // @todo: dodělat vlastní eventy, destroy.collapsable + // @todo: vyzkoušet? e.originalEvent.stopPropagation() v expand.collapsable callbacku by mohlo zabránit spouštění AJAXového požadavku v případě, že na .ca-control byla class ajax? + // @todo: add aria data attributes, see http://heydonworks.com/practical_aria_examples/ // @feature: díky předávání originalEvent do expand.collapsable (atd.) je možné použít e.originalEvent.preventDefault() místo defaults.preventDefault! cool, ne?! @@ -58,14 +57,14 @@ init: function(options) { return new Collapsable(this, options); }, - expandAll: function(event) { - handlePublicMethods.call(this, 'expandAll', event); + expandAll: function(data) { + handlePublicMethods.call(this, 'expandAll', data); }, - collapseAll: function(event) { - handlePublicMethods.call(this, 'collapseAll', event); + collapseAll: function(data) { + handlePublicMethods.call(this, 'collapseAll', data); }, - destroy: function(event) { - handlePublicMethods.call(this, 'destroy', event); // @todo destroy :) + destroy: function(data) { + handlePublicMethods.call(this, 'destroy', data); // @todo destroy :) } }; @@ -91,10 +90,10 @@ /** * Handles public method called on jQuery object using adapter * @param {String} action - CollapseAll|expandAll|destroy @todo destroy - * @param {Object} event - Event (object) passed by user + * @param {Object} data - Data passed by user * @private */ - function handlePublicMethods(action, event) { + function handlePublicMethods(action, data) { var processed = []; this.each(function() { var instance = $(this).data('collapsable'); @@ -104,7 +103,7 @@ if (processed.indexOf(uid) === -1) { processed.push(uid); - instance.parent[action](event); + instance.parent[action](data); } } }); @@ -202,7 +201,7 @@ * @private */ function handleDefaultExpanded() { - var event; + var event = $.Event('init.collapsable'); var opts = this.opts; var items = this.items; @@ -214,14 +213,13 @@ 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, @todo 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? + var force = ! opts.collapsableAll; // if we can't collapse all, we force expanding the first one chosen in prepareDefaultExpanded, @todo 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) for (var i = 0; i < l; i++) { - event = $.Event('init.collapsable', items[i].$collapsable); // @todo: dává toto smysl? neměla by event být společná? ukládá se opravdu do originalEvent? if (items[i].defaultExpanded) { - items[i].expand(event, force); + items[i].expand(event, null, force); force = false; } else { - items[i].collapse(event, true); // on init, we want to close all regardless - if you return false, than we shouln't have the class set in first place + items[i].collapse(event, null, true); // on init, we want to force-close all - if you return false, than you should have the class set in first place (so it would go into if statement above) } } @@ -313,21 +311,21 @@ /** * Expands all collapsed items - * @param {Event} event - Event to be passed to onExpand and onExpanded callbacks + * @param {Object} data - Data to be passed to triggered event */ - Collapsable.prototype.expandAll = function(event) { + Collapsable.prototype.expandAll = function(data) { // if grouped, we only want to expand one (first) box, or none if already expanded if (this.opts.grouped && this.getExpanded().length) { return; } - event = event || { type: 'collapsable.expandAll' }; + 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); + var expanded = this.items[i].expand(event, data); if (this.opts.grouped && expanded) { break; @@ -339,16 +337,16 @@ /** * Collapses all expanded items - * @param {Event} event - Event to be passed to onCollapse and onCollapsed callbacks + * @param {Object} data - Data to be passed to triggered event */ - Collapsable.prototype.collapseAll = function(event) { - event = event || { type: 'collapsable.collapseAll' }; + 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); + this.items[expandedItems[i]].collapse(event, data); } }; @@ -387,7 +385,7 @@ selector = (this.$control[0].tagName.toUpperCase() === 'A') ? opts.control : opts.control + ' a'; - // data contains arguments passed when trigger is called, used for passing event that triggered opening (eg. extLink click) + // originalEvent contains arguments passed when trigger is called, used for passing event that triggered opening (eg. extLink click) this.$collapsable.on(opts.event, selector, function(event, originalEvent) { var passEvent = originalEvent ? originalEvent : event; if (opts.preventDefault) { @@ -459,32 +457,32 @@ /** * Expands single CollapsableItem; could be prevented by returning false from onExpand callback - * @param {Object} originalEvent - Event passed to function + * @param {Object} originalEvent - 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(originalEvent, force) { + CollapsableItem.prototype.expand = function(originalEvent, data, force) { var opts = this.parent.opts; var expandedItem = this.parent.getExpanded(); // grouped -> max one expanded item this.parent.promiseOpen = true; // allows us to collapse expanded item even if there might be collapseAll === false option if (opts.grouped) { - // before expanding, we have to collapse previously opened item - // if grouped element hasn't collapsed, we can't continue - if (expandedItem.length && this.parent.items[expandedItem[0]].collapse(originalEvent, force) === false) { + // before expanding, we have to collapse previously opened item, if grouped element hasn't collapsed, we can't continue + if (expandedItem.length && this.parent.items[expandedItem[0]].collapse(originalEvent, data, force) === false) { this.parent.promiseOpen = false; return false; } } this.parent.promiseOpen = false; - var event = $.Event('expand.collapsable', { originalEvent: originalEvent }); + var event = $.Event('expand.collapsable', { customData: data, originalEvent: originalEvent }); this.$collapsable.trigger(event); if (event.isDefaultPrevented() && ! force) { // collapsableAll === false && grouped === true -> if the box has not opened, we must make sure something is opened, therefore we force-open previously opened box (opts.grouped is true means we tried to collapse something), simulating it has never closed in first place if (! opts.collapsableAll && opts.grouped) { - this.parent.items[expandedItem[0]].expand(originalEvent, true); + this.parent.items[expandedItem[0]].expand(originalEvent, data, true); } return false; @@ -495,18 +493,19 @@ /** * Collapses single CollapsableItem; could be prevented by returning false from onCollapse callback - * @param {Object} originalEvent - Event passed to function + * @param {Object} originalEvent - 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(originalEvent, force) { + CollapsableItem.prototype.collapse = function(originalEvent, 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', { originalEvent: originalEvent }); + var event = $.Event('collapse.collapsable', { customData: data, originalEvent: originalEvent }); this.$collapsable.trigger(event); if (event.isDefaultPrevented() && !force) { @@ -544,15 +543,6 @@ } else if (typeof options === 'object' || !options) { var data = methods.init.apply(this, arguments); - - // @todo: místo tohoto bude init.collapsable nad všemi položkami dohromady? - if (data && typeof data.opts.onInit === 'function') { - var l = data.items.length; - for (var i = 0; i < l; i++) { - data.opts.onInit.call(data.items[i].$collapsable); - } - } - return this; } else { From dc7d6be4a8932dab19cdba05a1a2990bf91adcb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20=C5=A0er=C3=BD?= Date: Sat, 27 Feb 2016 19:41:52 +0100 Subject: [PATCH 07/10] #0 v2.0.0 - RC - gh-pages tba --- jquery.collapsable.js | 177 +++++++++++++++++++++++++++----------- jquery.collapsable.min.js | 2 + 2 files changed, 130 insertions(+), 49 deletions(-) create mode 100644 jquery.collapsable.min.js diff --git a/jquery.collapsable.js b/jquery.collapsable.js index 5c11216..a26c98a 100644 --- a/jquery.collapsable.js +++ b/jquery.collapsable.js @@ -10,10 +10,6 @@ */ ;(function($) { - // @todo: dodělat vlastní eventy, destroy.collapsable - // @todo: vyzkoušet? e.originalEvent.stopPropagation() v expand.collapsable callbacku by mohlo zabránit spouštění AJAXového požadavku v případě, že na .ca-control byla class ajax? - // @todo: add aria data attributes, see http://heydonworks.com/practical_aria_examples/ - // @feature: díky předávání originalEvent do expand.collapsable (atd.) je možné použít e.originalEvent.preventDefault() místo defaults.preventDefault! cool, ne?! /** @@ -26,26 +22,25 @@ event: 'click', // event triggering the expand/collapse fx: false, // effect for expanding/collapsing, [ false | toggle | slide | fade | {Object} ] - fxDuration: 0, // duration of the effect, affects delay between onExpand (onCollapse) and onExpanded (onCollapsed) callbacks; default value is 500 when fx set to slide + 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 grouped: 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 - preventDefault: true, // whether prevenDefault should be called when specified event occurs on control; even if false, you may use e.originalEvent.preventDefault() inside collapsable event handlers + preventDefault: true, // whether prevenDefault should be called when specified event occurs on control; even if false, e.originalEvent.preventDefault() may be used inside collapsable event handlers extLinks: { // external links for operating collapsable set, can be anywhere else in DOM selector: null, // CSS selector for external links; it has to be anchors; the click event is binded - //openOnly: true // @todo create possibility for extLinks would only open the boxes? for now, you might achieve this using callbacks and returning false preventDefault: false, // whether preventDefault is called on extLinks click activeClass: 'ca-ext-active' // class which would be toggled on external link when associated box is expanded or collapsed }, 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', + expanded: 'ca-expanded', + collapsed: 'ca-collapsed', defaultExpanded: 'ca-default-expanded' } - // callbacks are no more available, use events instead + // callbacks are no more available, use event handlers instead }; @@ -54,17 +49,17 @@ * @type {{init: methods.init, expandAll: methods.expandAll, collapseAll: methods.collapseAll, destroy: methods.destroy}} */ var methods = { - init: function(options) { + init: function (options) { return new Collapsable(this, options); }, - expandAll: function(data) { + expandAll: function (data) { handlePublicMethods.call(this, 'expandAll', data); }, - collapseAll: function(data) { + collapseAll: function (data) { handlePublicMethods.call(this, 'collapseAll', data); }, - destroy: function(data) { - handlePublicMethods.call(this, 'destroy', data); // @todo destroy :) + destroy: function (data) { + handlePublicMethods.call(this, 'destroy', data); } }; @@ -89,13 +84,13 @@ /** * Handles public method called on jQuery object using adapter - * @param {String} action - CollapseAll|expandAll|destroy @todo destroy + * @param {String} action - collapseAll|expandAll|destroy * @param {Object} data - Data passed by user * @private */ function handlePublicMethods(action, data) { var processed = []; - this.each(function() { + this.each(function () { var instance = $(this).data('collapsable'); if (instance) { var uid = instance.parent.uid; @@ -113,20 +108,22 @@ /** * 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() { - if ((this.$extLinks = $(this.opts.extLinks.selector).filter('a')).length) { + var opts = this.opts; + if ((this.$extLinks = $(opts.extLinks.selector).filter('a')).length) { var that = this; - this.$extLinks.on('click', function(event) { - if (that.opts.extLinks.preventDefault) + this.$extLinks.on('click.collapsable', function (event) { + if (opts.extLinks.preventDefault) event.preventDefault(); var collapsable = $($(this).attr('href')).data('collapsable'); if (collapsable) { - collapsable.$control.find('a').trigger('click', event); + collapsable.$controlLink.first().trigger(opts.event + '.collapsable', event); } }); } @@ -148,7 +145,9 @@ var defaultExpanded = -1; var defaultExpandedFromUrl = -1; - var $items = $($.map(this.items, function(item){return item.$collapsable.get();})); + var $items = $($.map(this.items, function (item) { + return item.$collapsable.get(); + })); // search for #hash in url if (i !== -1) { @@ -179,16 +178,16 @@ // not grouped, we add flag to all items with class else { var that = this; - $items.each(function(i) { + $items.each(function (i) { if ($(this).hasClass(that.opts.classNames.defaultExpanded)) { - defaultExpanded = i; // for later use is sufficient to have last index only + 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) { + if (defaultExpandedFromUrl === -1 && defaultExpanded === -1 && !this.opts.collapsableAll) { this.items[0].defaultExpanded = true; } } @@ -197,6 +196,7 @@ /** * Expand or collapse item based on flags set in prepareDefaultExpanded method; called on initialization within the * context of Collapsable + * @todo When !opts.collapseAll && opts.grouped, 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 */ @@ -213,13 +213,13 @@ 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, @todo 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) + 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 you return false, than you should have the class set in first place (so it would go into if statement above) + 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) } } @@ -264,7 +264,7 @@ * @returns {Collapsable} * @constructor */ - var Collapsable = function($boxSet, options) { + var Collapsable = function ($boxSet, options) { this.opts = $.extend(true, {}, $.fn.collapsable.defaults, options); this.items = []; @@ -277,7 +277,7 @@ prepareFxOpt.call(this); - $boxSet.each(function() { + $boxSet.each(function () { var collapsable = new CollapsableItem(that, this); if (collapsable.$box.length && collapsable.$control.length) { @@ -295,7 +295,7 @@ * Returns indexes of expanded items in Collapsable.items * @returns {Array} */ - Collapsable.prototype.getExpanded = function() { + Collapsable.prototype.getExpanded = function () { var expanded = []; var l = this.items.length; @@ -313,7 +313,7 @@ * Expands all collapsed items * @param {Object} data - Data to be passed to triggered event */ - Collapsable.prototype.expandAll = function(data) { + Collapsable.prototype.expandAll = function (data) { // if grouped, we only want to expand one (first) box, or none if already expanded if (this.opts.grouped && this.getExpanded().length) { return; @@ -324,7 +324,7 @@ var l = this.items.length; for (var i = 0; i < l; i++) { - if (! this.items[i].isExpanded()) { + if (!this.items[i].isExpanded()) { var expanded = this.items[i].expand(event, data); if (this.opts.grouped && expanded) { @@ -339,7 +339,7 @@ * Collapses all expanded items * @param {Object} data - Data to be passed to triggered event */ - Collapsable.prototype.collapseAll = function(data) { + Collapsable.prototype.collapseAll = function (data) { var event = $.Event('collapseAll.collapsable'); var expandedItems = this.getExpanded(); @@ -351,6 +351,90 @@ }; + /** + * 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') + .off(opts.event + '.collapsable'); + + // revert control element + item.$control.removeClass('ca-link'); + item.$control.find('.ca-link').removeClass('ca-link'); + item.$control.find('[data-ca-created]').each(function () { + $(this).parent().html($(this).html()); + }); + + // revert box element + var style = item.$box.data('ca-pre-init-style') || ''; + item.$box + .attr('style', style) + .removeData('ca-pre-init-style'); + + 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 = $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 @@ -372,21 +456,12 @@ var selector; // selector for clickable element var opts = this.parent.opts; // shortcut - if (! this.id) { - this.id = getUid(); - this.$collapsable.attr('id', this.id); - } + prepareCollapsableDOM.call(this); - // @todo umožnit různou strukturu ca-control, nyní musí být vždy stejná, tj. nelze

... a

...

v jednom boxu - nevytvoří se odkaz ve druhém elementu - // souvisí s tím i hodnota proměnné selector níže - if (this.$control.find('a').length === 0 && this.$control[0].tagName.toUpperCase() != 'A') { - this.$control.wrapInner(''); - } - - selector = (this.$control[0].tagName.toUpperCase() === 'A') ? opts.control : opts.control + ' a'; + this.$controlLink = this.$control.find('.ca-link'); // originalEvent contains arguments passed when trigger is called, used for passing event that triggered opening (eg. extLink click) - this.$collapsable.on(opts.event, selector, function(event, originalEvent) { + this.$controlLink.on(opts.event + '.collapsable', function(event, originalEvent) { var passEvent = originalEvent ? originalEvent : event; if (opts.preventDefault) { event.preventDefault(); @@ -407,9 +482,9 @@ /** - * + * Handling common parts of expanding and collapsing * @param {String} action - Either `expand` or `collapse` - * @param {Event} event - Event to be passed to callbacks + * @param {Event} event - Event to be passed to event handlers * @returns {boolean} */ function handleExpandCollapse(action, event) { @@ -436,6 +511,10 @@ .removeClass(removeClass) .addClass(addClass); + // aria support + this.$controlLink.attr('aria-expanded', action === 'expand'); + this.$box.attr('aria-hidden', action !== 'expand'); + // actually toggle the box state if(typeof opts.fx === 'object') { this.$box[opts.fx[action]](opts.fxDuration, function () { @@ -456,7 +535,7 @@ } /** - * Expands single CollapsableItem; could be prevented by returning false from onExpand callback + * Expands single CollapsableItem; could be prevented by `preventDefault` called on `expand.collapsable` event * @param {Object} originalEvent - 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) @@ -492,7 +571,7 @@ }; /** - * Collapses single CollapsableItem; could be prevented by returning false from onCollapse callback + * Collapses single CollapsableItem; could be prevented by `preventDefault` called on `collapse.collapsable` event * @param {Object} originalEvent - 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 diff --git a/jquery.collapsable.min.js b/jquery.collapsable.min.js new file mode 100644 index 0000000..9c641a3 --- /dev/null +++ b/jquery.collapsable.min.js @@ -0,0 +1,2 @@ +/* jquery.collapsable v2.0.0 | MIT License | Radek Šerý */ +!function(t){function e(){return"ca-uid-"+d++}function a(e,a){var l=[];this.each(function(){var s=t(this).data("collapsable");if(s){var i=s.parent.uid;-1===l.indexOf(i)&&(l.push(i),s.parent[e](a))}})}function l(){var e=this.opts;if((this.$extLinks=t(e.extLinks.selector).filter("a")).length){this.$extLinks.on("click.collapsable",function(a){e.extLinks.preventDefault&&a.preventDefault();var l=t(t(this).attr("href")).data("collapsable");l&&l.$controlLink.first().trigger(e.event+".collapsable",a)})}}function s(){var e=window.location.href,a=e.search(/#/),l=-1,s=-1,i=t(t.map(this.items,function(t){return t.$collapsable.get()}));if(-1===a||(e=e.substring(a+1),s=i.index(t("#"+e)),-1===s||(this.items[s].defaultExpanded=!0,!this.opts.grouped))){if(this.opts.grouped){if(l=i.index(t("."+this.opts.classNames.defaultExpanded)),-1!==l)return void(this.items[l].defaultExpanded=!0)}else{var n=this;i.each(function(e){t(this).hasClass(n.opts.classNames.defaultExpanded)&&(l=e,n.items[e].defaultExpanded=!0)})}-1!==s||-1!==l||this.opts.collapsableAll||(this.items[0].defaultExpanded=!0)}}function i(){var e=t.Event("init.collapsable"),a=this.opts,l=this.items,s=a.fx;a.fx&&(a.fx="toggle");for(var i=l.length,n=!a.collapsableAll,o=0;i>o;o++)l[o].defaultExpanded?(l[o].expand(e,null,n),n=!1):l[o].collapse(e,null,!0);a.fx=s}function n(){var t=this.opts;"slide"===t.fx?t.fx={expand:"slideDown",collapse:"slideUp"}:"fade"===this.opts.fx&&(t.fx={expand:"fadeIn",collapse:"fadeOut"}),("slide"===t.fx||"fade"===t.fx)&&(t.fxDuration=t.fxDuration||500)}function o(){var a=this;a.id||(a.id=e(),a.$collapsable.attr("id",this.id));var l=a.$box.attr("id")||a.id+"-ca-box";a.$box.attr("id",l).data("ca-pre-init-style",a.$box.attr("style")),a.$control.each(function(){var e,s=t(this);s.is("a")?e=s:(e=s.find("a")).length||(s.wrapInner(''),e=s.find("a")),e.addClass("ca-link"),e.attr("aria-controls",l)})}function r(t,e){var a=this.parent.opts,l=this,s="expanded",i=a.classNames.expanded,n=a.classNames.collapsed;if("collapse"===t&&(s="collapsed",i=a.classNames.collapsed,n=a.classNames.expanded),this.parent.$extLinks.filter("[href=#"+this.id+"]")["expand"===t?"addClass":"removeClass"](a.extLinks.activeClass),this.$collapsable.removeClass(n).addClass(i),this.$controlLink.attr("aria-expanded","expand"===t),this.$box.attr("aria-hidden","expand"!==t),"object"==typeof a.fx)this.$box[a.fx[t]](a.fxDuration,function(){l.$collapsable.trigger(s+".collapsable")});else{"toggle"==a.fx&&this.$box["expand"===t?"show":"hide"]();setTimeout(function(){l.$collapsable.trigger(s+".collapsable")},a.fxDuration)}return!0}var p={control:".ca-control",box:".ca-box",event:"click",fx:!1,fxDuration:0,grouped:!1,collapsableAll:!0,preventDefault:!0,extLinks:{selector:null,preventDefault:!1,activeClass:"ca-ext-active"},classNames:{expanded:"ca-expanded",collapsed:"ca-collapsed",defaultExpanded:"ca-default-expanded"}},c={init:function(t){return new h(this,t)},expandAll:function(t){a.call(this,"expandAll",t)},collapseAll:function(t){a.call(this,"collapseAll",t)},destroy:function(t){a.call(this,"destroy",t)}},d=0,h=function(a,o){this.opts=t.extend(!0,{},t.fn.collapsable.defaults,o),this.items=[];var r=this;return this.uid=e(),this.promiseOpen=!1,l.call(this),n.call(this),a.each(function(){var t=new f(r,this);t.$box.length&&t.$control.length&&r.items.push(t)}),s.call(this),i.call(this),this};h.prototype.getExpanded=function(){for(var t=[],e=this.items.length,a=0;e>a;a++)this.items[a].isExpanded()&&t.push(a);return t},h.prototype.expandAll=function(e){if(!this.opts.grouped||!this.getExpanded().length)for(var a=t.Event("expandAll.collapsable"),l=this.items.length,s=0;l>s;s++)if(!this.items[s].isExpanded()){var i=this.items[s].expand(a,e);if(this.opts.grouped&&i)break}},h.prototype.collapseAll=function(e){for(var a=t.Event("collapseAll.collapsable"),l=this.getExpanded(),s=l.length,i=0;s>i;i++)this.items[l[i]].collapse(a,e)},h.prototype.destroy=function(){for(var e=this.opts,a=this.items.length,l=0;a>l;l++){var s=this.items[l];s.$collapsable.removeClass(e.classNames.collapsed+" "+e.classNames.expanded).removeData("collapsable").off(e.event+".collapsable"),s.$control.removeClass("ca-link"),s.$control.find(".ca-link").removeClass("ca-link"),s.$control.find("[data-ca-created]").each(function(){t(this).parent().html(t(this).html())});var i=s.$box.data("ca-pre-init-style")||"";s.$box.attr("style",i).removeData("ca-pre-init-style"),s.$collapsable.trigger("destroy.collapsable")}this.$extLinks.off("click.collapsable")};var f=function(e,a){if(this.parent=e,this.$collapsable=t(a),this.id=this.$collapsable.attr("id"),this.$control=this.$collapsable.find(e.opts.control),this.$box=this.$collapsable.find(e.opts.box),0!=this.$control.length&&0!=this.$box.length){var l=this,s=this.parent.opts;return o.call(this),this.$controlLink=this.$control.find(".ca-link"),this.$controlLink.on(s.event+".collapsable",function(t,e){var a=e?e:t;s.preventDefault&&t.preventDefault(),l.isExpanded()?l.collapse(a):l.expand(a)}),this.$collapsable.data("collapsable",this),this}};f.prototype.expand=function(e,a,l){var s=this.parent.opts,i=this.parent.getExpanded();if(this.parent.promiseOpen=!0,s.grouped&&i.length&&this.parent.items[i[0]].collapse(e,a,l)===!1)return this.parent.promiseOpen=!1,!1;this.parent.promiseOpen=!1;var n=t.Event("expand.collapsable",{customData:a,originalEvent:e});return this.$collapsable.trigger(n),n.isDefaultPrevented()&&!l?(!s.collapsableAll&&s.grouped&&this.parent.items[i[0]].expand(e,a,!0),!1):r.call(this,"expand",e)},f.prototype.collapse=function(e,a,l){var s=this.parent.opts;if(!s.collapsableAll&&!this.parent.promiseOpen&&this.parent.getExpanded().length<2)return!1;var i=t.Event("collapse.collapsable",{customData:a,originalEvent:e});return this.$collapsable.trigger(i),i.isDefaultPrevented()&&!l?!1:r.call(this,"collapse",e)},f.prototype.isExpanded=function(){return this.$collapsable.hasClass(this.parent.opts.classNames.expanded)},t.fn.collapsable=function(e){if(c[e])return c[e].apply(this,Array.prototype.slice.call(arguments,1));if("object"==typeof e||!e){c.init.apply(this,arguments);return this}t.error("Method "+e+" does not exist on jQuery.collapsable")},t.fn.collapsable.defaults=p}(jQuery); From bd617406f38d5e33b205de0bb027a014f66feef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20=C5=A0er=C3=BD?= Date: Sat, 27 Feb 2016 19:59:10 +0100 Subject: [PATCH 08/10] #0 v2.0.0 debug, optimization - RC - gh-pages tba --- jquery.collapsable.js | 29 +++++++++++++++-------------- jquery.collapsable.min.js | 2 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/jquery.collapsable.js b/jquery.collapsable.js index a26c98a..aa74330 100644 --- a/jquery.collapsable.js +++ b/jquery.collapsable.js @@ -114,8 +114,6 @@ function handleExtLinks() { var opts = this.opts; if ((this.$extLinks = $(opts.extLinks.selector).filter('a')).length) { - var that = this; - this.$extLinks.on('click.collapsable', function (event) { if (opts.extLinks.preventDefault) event.preventDefault(); @@ -238,6 +236,11 @@ var opts = this.opts; // default fxDuration in case of slide function + if (opts.fx === 'slide' || opts.fx === 'fade') { + opts.fxDuration = opts.fxDuration || 500; + } + + // convert alias into objects if (opts.fx === 'slide') { opts.fx = { expand: 'slideDown', @@ -249,10 +252,6 @@ collapse: 'fadeOut' }; } - - if (opts.fx === 'slide' || opts.fx === 'fade') { - opts.fxDuration = opts.fxDuration || 500; - } } @@ -453,7 +452,6 @@ return; var that = this; - var selector; // selector for clickable element var opts = this.parent.opts; // shortcut prepareCollapsableDOM.call(this); @@ -484,10 +482,11 @@ /** * Handling common parts of expanding and collapsing * @param {String} action - Either `expand` or `collapse` - * @param {Event} event - Event to be passed to event handlers + * @param {Object} data - Data passed to triggered event + * @param {Event} originalEvent - Event to be passed to event handlers * @returns {boolean} */ - function handleExpandCollapse(action, event) { + function handleExpandCollapse(action, originalEvent, data) { var opts = this.parent.opts; var that = this; var trigger = 'expanded'; @@ -501,6 +500,8 @@ removeClass = opts.classNames.expanded; } + var event = $.Event(trigger + '.collapsable', { customData: data, originalEvent: originalEvent }); + // update extLinks this.parent.$extLinks .filter('[href=#' + this.id + ']') @@ -518,7 +519,7 @@ // actually toggle the box state if(typeof opts.fx === 'object') { this.$box[opts.fx[action]](opts.fxDuration, function () { - that.$collapsable.trigger(trigger + '.collapsable'); + that.$collapsable.trigger(event); }); } else { @@ -526,8 +527,8 @@ this.$box[action === 'expand' ? 'show' : 'hide'](); } - var t = setTimeout(function () { - that.$collapsable.trigger(trigger + '.collapsable'); + setTimeout(function () { + that.$collapsable.trigger(event); }, opts.fxDuration); } @@ -591,7 +592,7 @@ return false; } - return handleExpandCollapse.call(this, 'collapse', originalEvent) + return handleExpandCollapse.call(this, 'collapse', originalEvent); }; /** @@ -621,7 +622,7 @@ return methods[options].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof options === 'object' || !options) { - var data = methods.init.apply(this, arguments); + methods.init.apply(this, arguments); return this; } else { diff --git a/jquery.collapsable.min.js b/jquery.collapsable.min.js index 9c641a3..2a50eaf 100644 --- a/jquery.collapsable.min.js +++ b/jquery.collapsable.min.js @@ -1,2 +1,2 @@ /* jquery.collapsable v2.0.0 | MIT License | Radek Šerý */ -!function(t){function e(){return"ca-uid-"+d++}function a(e,a){var l=[];this.each(function(){var s=t(this).data("collapsable");if(s){var i=s.parent.uid;-1===l.indexOf(i)&&(l.push(i),s.parent[e](a))}})}function l(){var e=this.opts;if((this.$extLinks=t(e.extLinks.selector).filter("a")).length){this.$extLinks.on("click.collapsable",function(a){e.extLinks.preventDefault&&a.preventDefault();var l=t(t(this).attr("href")).data("collapsable");l&&l.$controlLink.first().trigger(e.event+".collapsable",a)})}}function s(){var e=window.location.href,a=e.search(/#/),l=-1,s=-1,i=t(t.map(this.items,function(t){return t.$collapsable.get()}));if(-1===a||(e=e.substring(a+1),s=i.index(t("#"+e)),-1===s||(this.items[s].defaultExpanded=!0,!this.opts.grouped))){if(this.opts.grouped){if(l=i.index(t("."+this.opts.classNames.defaultExpanded)),-1!==l)return void(this.items[l].defaultExpanded=!0)}else{var n=this;i.each(function(e){t(this).hasClass(n.opts.classNames.defaultExpanded)&&(l=e,n.items[e].defaultExpanded=!0)})}-1!==s||-1!==l||this.opts.collapsableAll||(this.items[0].defaultExpanded=!0)}}function i(){var e=t.Event("init.collapsable"),a=this.opts,l=this.items,s=a.fx;a.fx&&(a.fx="toggle");for(var i=l.length,n=!a.collapsableAll,o=0;i>o;o++)l[o].defaultExpanded?(l[o].expand(e,null,n),n=!1):l[o].collapse(e,null,!0);a.fx=s}function n(){var t=this.opts;"slide"===t.fx?t.fx={expand:"slideDown",collapse:"slideUp"}:"fade"===this.opts.fx&&(t.fx={expand:"fadeIn",collapse:"fadeOut"}),("slide"===t.fx||"fade"===t.fx)&&(t.fxDuration=t.fxDuration||500)}function o(){var a=this;a.id||(a.id=e(),a.$collapsable.attr("id",this.id));var l=a.$box.attr("id")||a.id+"-ca-box";a.$box.attr("id",l).data("ca-pre-init-style",a.$box.attr("style")),a.$control.each(function(){var e,s=t(this);s.is("a")?e=s:(e=s.find("a")).length||(s.wrapInner(''),e=s.find("a")),e.addClass("ca-link"),e.attr("aria-controls",l)})}function r(t,e){var a=this.parent.opts,l=this,s="expanded",i=a.classNames.expanded,n=a.classNames.collapsed;if("collapse"===t&&(s="collapsed",i=a.classNames.collapsed,n=a.classNames.expanded),this.parent.$extLinks.filter("[href=#"+this.id+"]")["expand"===t?"addClass":"removeClass"](a.extLinks.activeClass),this.$collapsable.removeClass(n).addClass(i),this.$controlLink.attr("aria-expanded","expand"===t),this.$box.attr("aria-hidden","expand"!==t),"object"==typeof a.fx)this.$box[a.fx[t]](a.fxDuration,function(){l.$collapsable.trigger(s+".collapsable")});else{"toggle"==a.fx&&this.$box["expand"===t?"show":"hide"]();setTimeout(function(){l.$collapsable.trigger(s+".collapsable")},a.fxDuration)}return!0}var p={control:".ca-control",box:".ca-box",event:"click",fx:!1,fxDuration:0,grouped:!1,collapsableAll:!0,preventDefault:!0,extLinks:{selector:null,preventDefault:!1,activeClass:"ca-ext-active"},classNames:{expanded:"ca-expanded",collapsed:"ca-collapsed",defaultExpanded:"ca-default-expanded"}},c={init:function(t){return new h(this,t)},expandAll:function(t){a.call(this,"expandAll",t)},collapseAll:function(t){a.call(this,"collapseAll",t)},destroy:function(t){a.call(this,"destroy",t)}},d=0,h=function(a,o){this.opts=t.extend(!0,{},t.fn.collapsable.defaults,o),this.items=[];var r=this;return this.uid=e(),this.promiseOpen=!1,l.call(this),n.call(this),a.each(function(){var t=new f(r,this);t.$box.length&&t.$control.length&&r.items.push(t)}),s.call(this),i.call(this),this};h.prototype.getExpanded=function(){for(var t=[],e=this.items.length,a=0;e>a;a++)this.items[a].isExpanded()&&t.push(a);return t},h.prototype.expandAll=function(e){if(!this.opts.grouped||!this.getExpanded().length)for(var a=t.Event("expandAll.collapsable"),l=this.items.length,s=0;l>s;s++)if(!this.items[s].isExpanded()){var i=this.items[s].expand(a,e);if(this.opts.grouped&&i)break}},h.prototype.collapseAll=function(e){for(var a=t.Event("collapseAll.collapsable"),l=this.getExpanded(),s=l.length,i=0;s>i;i++)this.items[l[i]].collapse(a,e)},h.prototype.destroy=function(){for(var e=this.opts,a=this.items.length,l=0;a>l;l++){var s=this.items[l];s.$collapsable.removeClass(e.classNames.collapsed+" "+e.classNames.expanded).removeData("collapsable").off(e.event+".collapsable"),s.$control.removeClass("ca-link"),s.$control.find(".ca-link").removeClass("ca-link"),s.$control.find("[data-ca-created]").each(function(){t(this).parent().html(t(this).html())});var i=s.$box.data("ca-pre-init-style")||"";s.$box.attr("style",i).removeData("ca-pre-init-style"),s.$collapsable.trigger("destroy.collapsable")}this.$extLinks.off("click.collapsable")};var f=function(e,a){if(this.parent=e,this.$collapsable=t(a),this.id=this.$collapsable.attr("id"),this.$control=this.$collapsable.find(e.opts.control),this.$box=this.$collapsable.find(e.opts.box),0!=this.$control.length&&0!=this.$box.length){var l=this,s=this.parent.opts;return o.call(this),this.$controlLink=this.$control.find(".ca-link"),this.$controlLink.on(s.event+".collapsable",function(t,e){var a=e?e:t;s.preventDefault&&t.preventDefault(),l.isExpanded()?l.collapse(a):l.expand(a)}),this.$collapsable.data("collapsable",this),this}};f.prototype.expand=function(e,a,l){var s=this.parent.opts,i=this.parent.getExpanded();if(this.parent.promiseOpen=!0,s.grouped&&i.length&&this.parent.items[i[0]].collapse(e,a,l)===!1)return this.parent.promiseOpen=!1,!1;this.parent.promiseOpen=!1;var n=t.Event("expand.collapsable",{customData:a,originalEvent:e});return this.$collapsable.trigger(n),n.isDefaultPrevented()&&!l?(!s.collapsableAll&&s.grouped&&this.parent.items[i[0]].expand(e,a,!0),!1):r.call(this,"expand",e)},f.prototype.collapse=function(e,a,l){var s=this.parent.opts;if(!s.collapsableAll&&!this.parent.promiseOpen&&this.parent.getExpanded().length<2)return!1;var i=t.Event("collapse.collapsable",{customData:a,originalEvent:e});return this.$collapsable.trigger(i),i.isDefaultPrevented()&&!l?!1:r.call(this,"collapse",e)},f.prototype.isExpanded=function(){return this.$collapsable.hasClass(this.parent.opts.classNames.expanded)},t.fn.collapsable=function(e){if(c[e])return c[e].apply(this,Array.prototype.slice.call(arguments,1));if("object"==typeof e||!e){c.init.apply(this,arguments);return this}t.error("Method "+e+" does not exist on jQuery.collapsable")},t.fn.collapsable.defaults=p}(jQuery); +!function(t){function e(){return"ca-uid-"+d++}function a(e,a){var l=[];this.each(function(){var s=t(this).data("collapsable");if(s){var i=s.parent.uid;-1===l.indexOf(i)&&(l.push(i),s.parent[e](a))}})}function l(){var e=this.opts;(this.$extLinks=t(e.extLinks.selector).filter("a")).length&&this.$extLinks.on("click.collapsable",function(a){e.extLinks.preventDefault&&a.preventDefault();var l=t(t(this).attr("href")).data("collapsable");l&&l.$controlLink.first().trigger(e.event+".collapsable",a)})}function s(){var e=window.location.href,a=e.search(/#/),l=-1,s=-1,i=t(t.map(this.items,function(t){return t.$collapsable.get()}));if(-1===a||(e=e.substring(a+1),s=i.index(t("#"+e)),-1===s||(this.items[s].defaultExpanded=!0,!this.opts.grouped))){if(this.opts.grouped){if(l=i.index(t("."+this.opts.classNames.defaultExpanded)),-1!==l)return void(this.items[l].defaultExpanded=!0)}else{var n=this;i.each(function(e){t(this).hasClass(n.opts.classNames.defaultExpanded)&&(l=e,n.items[e].defaultExpanded=!0)})}-1!==s||-1!==l||this.opts.collapsableAll||(this.items[0].defaultExpanded=!0)}}function i(){var e=t.Event("init.collapsable"),a=this.opts,l=this.items,s=a.fx;a.fx&&(a.fx="toggle");for(var i=l.length,n=!a.collapsableAll,o=0;i>o;o++)l[o].defaultExpanded?(l[o].expand(e,null,n),n=!1):l[o].collapse(e,null,!0);a.fx=s}function n(){var t=this.opts;("slide"===t.fx||"fade"===t.fx)&&(t.fxDuration=t.fxDuration||500),"slide"===t.fx?t.fx={expand:"slideDown",collapse:"slideUp"}:"fade"===this.opts.fx&&(t.fx={expand:"fadeIn",collapse:"fadeOut"})}function o(){var a=this;a.id||(a.id=e(),a.$collapsable.attr("id",this.id));var l=a.$box.attr("id")||a.id+"-ca-box";a.$box.attr("id",l).data("ca-pre-init-style",a.$box.attr("style")),a.$control.each(function(){var e,s=t(this);s.is("a")?e=s:(e=s.find("a")).length||(s.wrapInner(''),e=s.find("a")),e.addClass("ca-link"),e.attr("aria-controls",l)})}function r(e,a,l){var s=this.parent.opts,i=this,n="expanded",o=s.classNames.expanded,r=s.classNames.collapsed;"collapse"===e&&(n="collapsed",o=s.classNames.collapsed,r=s.classNames.expanded);var p=t.Event(n+".collapsable",{customData:l,originalEvent:a});return this.parent.$extLinks.filter("[href=#"+this.id+"]")["expand"===e?"addClass":"removeClass"](s.extLinks.activeClass),this.$collapsable.removeClass(r).addClass(o),this.$controlLink.attr("aria-expanded","expand"===e),this.$box.attr("aria-hidden","expand"!==e),"object"==typeof s.fx?this.$box[s.fx[e]](s.fxDuration,function(){i.$collapsable.trigger(p)}):("toggle"==s.fx&&this.$box["expand"===e?"show":"hide"](),setTimeout(function(){i.$collapsable.trigger(p)},s.fxDuration)),!0}var p={control:".ca-control",box:".ca-box",event:"click",fx:!1,fxDuration:0,grouped:!1,collapsableAll:!0,preventDefault:!0,extLinks:{selector:null,preventDefault:!1,activeClass:"ca-ext-active"},classNames:{expanded:"ca-expanded",collapsed:"ca-collapsed",defaultExpanded:"ca-default-expanded"}},c={init:function(t){return new h(this,t)},expandAll:function(t){a.call(this,"expandAll",t)},collapseAll:function(t){a.call(this,"collapseAll",t)},destroy:function(t){a.call(this,"destroy",t)}},d=0,h=function(a,o){this.opts=t.extend(!0,{},t.fn.collapsable.defaults,o),this.items=[];var r=this;return this.uid=e(),this.promiseOpen=!1,l.call(this),n.call(this),a.each(function(){var t=new f(r,this);t.$box.length&&t.$control.length&&r.items.push(t)}),s.call(this),i.call(this),this};h.prototype.getExpanded=function(){for(var t=[],e=this.items.length,a=0;e>a;a++)this.items[a].isExpanded()&&t.push(a);return t},h.prototype.expandAll=function(e){if(!this.opts.grouped||!this.getExpanded().length)for(var a=t.Event("expandAll.collapsable"),l=this.items.length,s=0;l>s;s++)if(!this.items[s].isExpanded()){var i=this.items[s].expand(a,e);if(this.opts.grouped&&i)break}},h.prototype.collapseAll=function(e){for(var a=t.Event("collapseAll.collapsable"),l=this.getExpanded(),s=l.length,i=0;s>i;i++)this.items[l[i]].collapse(a,e)},h.prototype.destroy=function(){for(var e=this.opts,a=this.items.length,l=0;a>l;l++){var s=this.items[l];s.$collapsable.removeClass(e.classNames.collapsed+" "+e.classNames.expanded).removeData("collapsable").off(e.event+".collapsable"),s.$control.removeClass("ca-link"),s.$control.find(".ca-link").removeClass("ca-link"),s.$control.find("[data-ca-created]").each(function(){t(this).parent().html(t(this).html())});var i=s.$box.data("ca-pre-init-style")||"";s.$box.attr("style",i).removeData("ca-pre-init-style"),s.$collapsable.trigger("destroy.collapsable")}this.$extLinks.off("click.collapsable")};var f=function(e,a){if(this.parent=e,this.$collapsable=t(a),this.id=this.$collapsable.attr("id"),this.$control=this.$collapsable.find(e.opts.control),this.$box=this.$collapsable.find(e.opts.box),0!=this.$control.length&&0!=this.$box.length){var l=this,s=this.parent.opts;return o.call(this),this.$controlLink=this.$control.find(".ca-link"),this.$controlLink.on(s.event+".collapsable",function(t,e){var a=e?e:t;s.preventDefault&&t.preventDefault(),l.isExpanded()?l.collapse(a):l.expand(a)}),this.$collapsable.data("collapsable",this),this}};f.prototype.expand=function(e,a,l){var s=this.parent.opts,i=this.parent.getExpanded();if(this.parent.promiseOpen=!0,s.grouped&&i.length&&this.parent.items[i[0]].collapse(e,a,l)===!1)return this.parent.promiseOpen=!1,!1;this.parent.promiseOpen=!1;var n=t.Event("expand.collapsable",{customData:a,originalEvent:e});return this.$collapsable.trigger(n),n.isDefaultPrevented()&&!l?(!s.collapsableAll&&s.grouped&&this.parent.items[i[0]].expand(e,a,!0),!1):r.call(this,"expand",e)},f.prototype.collapse=function(e,a,l){var s=this.parent.opts;if(!s.collapsableAll&&!this.parent.promiseOpen&&this.parent.getExpanded().length<2)return!1;var i=t.Event("collapse.collapsable",{customData:a,originalEvent:e});return this.$collapsable.trigger(i),i.isDefaultPrevented()&&!l?!1:r.call(this,"collapse",e)},f.prototype.isExpanded=function(){return this.$collapsable.hasClass(this.parent.opts.classNames.expanded)},t.fn.collapsable=function(e){return c[e]?c[e].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof e&&e?void t.error("Method "+e+" does not exist on jQuery.collapsable"):(c.init.apply(this,arguments),this)},t.fn.collapsable.defaults=p}(jQuery); From a36a4d5bf003bb3dfacd31082cc9f46c5604c044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20=C5=A0er=C3=BD?= Date: Thu, 3 Mar 2016 21:46:49 +0100 Subject: [PATCH 09/10] #0 added package.json and Gruntfile.js to the project --- Gruntfile.js | 39 ++ bower.json | 25 +- .../jquery.collapsable.js | 4 +- dist/jquery.collapsable.min.js | 2 + jquery.collapsable.min.js | 2 - package.json | 25 + src/jquery.collapsable.js | 628 ++++++++++++++++++ 7 files changed, 714 insertions(+), 11 deletions(-) create mode 100644 Gruntfile.js rename jquery.collapsable.js => dist/jquery.collapsable.js (99%) create mode 100644 dist/jquery.collapsable.min.js delete mode 100644 jquery.collapsable.min.js create mode 100644 package.json create mode 100644 src/jquery.collapsable.js diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..b55c7af --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,39 @@ +module.exports = function(grunt) { + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + concat: { + options: { + stripBanners: true, + banner: '/**\n' + + ' * <%= pkg.title %> - <%= pkg.description %> \n' + + ' * <%= pkg.homepage %>\n' + + ' *\n' + + ' * @author <%= pkg.author.name %> <<%= pkg.author.email %>>\n' + + ' * @copyright Copyright (c) 2014-<%= grunt.template.today("yyyy") %> <%= pkg.author.name %>\n' + + ' * @license <%= pkg.license %>\n' + + ' *\n' + + ' * @version <%= pkg.version %>\n' + + ' */\n' + }, + production: { + src: ['src/jquery.collapsable.js'], + dest: 'dist/jquery.collapsable.js' + } + }, + uglify: { + options: { + banner: '/*! <%= pkg.title %> v<%= pkg.version %> | <%= pkg.license %> | <%= pkg.author.name %>, <%= pkg.homepage %> */\n' + }, + production: { + files: { + 'dist/jquery.collapsable.min.js': ['dist/jquery.collapsable.js'] + } + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + + grunt.registerTask('default', ['concat', 'uglify']); +}; diff --git a/bower.json b/bower.json index 28ac223..55940e8 100644 --- a/bower.json +++ b/bower.json @@ -1,14 +1,25 @@ { "name": "jquery.collapsable", - "version": "1.1.0", - "homepage": "https://github.com/peckadesign/jquery.collapsable", - "authors": [ - "PeckaDesign, s.r.o ", - "Radek Šerý " - ], + "description": "jQuery plugin for collapsable boxes", + "version": "2.0.0", "main": "jquery.collapsable.js", + "authors": [ "Radek Šerý " ], + "homepage": "http://peckadesign.github.io/jquery.collapsable/", "license": "MIT", + "keywords": ["jquery", "jquery-plugin", "collapsable", "jquery-collapsable"], "dependencies": { "jquery": ">=1.7" - } + }, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "demo", + "libs", + "src", + "test", + "*.md", + "Gruntfile.js", + "*.json" + ] } diff --git a/jquery.collapsable.js b/dist/jquery.collapsable.js similarity index 99% rename from jquery.collapsable.js rename to dist/jquery.collapsable.js index aa74330..68127ad 100644 --- a/jquery.collapsable.js +++ b/dist/jquery.collapsable.js @@ -1,12 +1,12 @@ /** - * jQuery plugin for collapsable boxes + * jQuery Collapsable - jQuery plugin for collapsable boxes + * http://peckadesign.github.io/jquery.collapsable/ * * @author Radek Šerý * @copyright Copyright (c) 2014-2016 Radek Šerý * @license MIT * * @version 2.0.0 - * */ ;(function($) { diff --git a/dist/jquery.collapsable.min.js b/dist/jquery.collapsable.min.js new file mode 100644 index 0000000..9de9b2c --- /dev/null +++ b/dist/jquery.collapsable.min.js @@ -0,0 +1,2 @@ +/*! jQuery Collapsable v2.0.0 | MIT | Radek Šerý, http://peckadesign.github.io/jquery.collapsable/ */ +!function(a){function b(){return"ca-uid-"+l++}function c(b,c){var d=[];this.each(function(){var e=a(this).data("collapsable");if(e){var f=e.parent.uid;-1===d.indexOf(f)&&(d.push(f),e.parent[b](c))}})}function d(){var b=this.opts;(this.$extLinks=a(b.extLinks.selector).filter("a")).length&&this.$extLinks.on("click.collapsable",function(c){b.extLinks.preventDefault&&c.preventDefault();var d=a(a(this).attr("href")).data("collapsable");d&&d.$controlLink.first().trigger(b.event+".collapsable",c)})}function e(){var b=window.location.href,c=b.search(/#/),d=-1,e=-1,f=a(a.map(this.items,function(a){return a.$collapsable.get()}));if(-1===c||(b=b.substring(c+1),e=f.index(a("#"+b)),-1===e||(this.items[e].defaultExpanded=!0,!this.opts.grouped))){if(this.opts.grouped){if(d=f.index(a("."+this.opts.classNames.defaultExpanded)),-1!==d)return void(this.items[d].defaultExpanded=!0)}else{var g=this;f.each(function(b){a(this).hasClass(g.opts.classNames.defaultExpanded)&&(d=b,g.items[b].defaultExpanded=!0)})}-1!==e||-1!==d||this.opts.collapsableAll||(this.items[0].defaultExpanded=!0)}}function f(){var b=a.Event("init.collapsable"),c=this.opts,d=this.items,e=c.fx;c.fx&&(c.fx="toggle");for(var f=d.length,g=!c.collapsableAll,h=0;f>h;h++)d[h].defaultExpanded?(d[h].expand(b,null,g),g=!1):d[h].collapse(b,null,!0);c.fx=e}function g(){var a=this.opts;"slide"!==a.fx&&"fade"!==a.fx||(a.fxDuration=a.fxDuration||500),"slide"===a.fx?a.fx={expand:"slideDown",collapse:"slideUp"}:"fade"===this.opts.fx&&(a.fx={expand:"fadeIn",collapse:"fadeOut"})}function h(){var c=this;c.id||(c.id=b(),c.$collapsable.attr("id",this.id));var d=c.$box.attr("id")||c.id+"-ca-box";c.$box.attr("id",d).data("ca-pre-init-style",c.$box.attr("style")),c.$control.each(function(){var b,e=a(this);e.is("a")?b=e:(b=e.find("a")).length||(e.wrapInner(''),b=e.find("a")),b.addClass("ca-link"),b.attr("aria-controls",d)})}function i(b,c,d){var e=this.parent.opts,f=this,g="expanded",h=e.classNames.expanded,i=e.classNames.collapsed;"collapse"===b&&(g="collapsed",h=e.classNames.collapsed,i=e.classNames.expanded);var j=a.Event(g+".collapsable",{customData:d,originalEvent:c});return this.parent.$extLinks.filter("[href=#"+this.id+"]")["expand"===b?"addClass":"removeClass"](e.extLinks.activeClass),this.$collapsable.removeClass(i).addClass(h),this.$controlLink.attr("aria-expanded","expand"===b),this.$box.attr("aria-hidden","expand"!==b),"object"==typeof e.fx?this.$box[e.fx[b]](e.fxDuration,function(){f.$collapsable.trigger(j)}):("toggle"==e.fx&&this.$box["expand"===b?"show":"hide"](),setTimeout(function(){f.$collapsable.trigger(j)},e.fxDuration)),!0}var j={control:".ca-control",box:".ca-box",event:"click",fx:!1,fxDuration:0,grouped:!1,collapsableAll:!0,preventDefault:!0,extLinks:{selector:null,preventDefault:!1,activeClass:"ca-ext-active"},classNames:{expanded:"ca-expanded",collapsed:"ca-collapsed",defaultExpanded:"ca-default-expanded"}},k={init:function(a){return new m(this,a)},expandAll:function(a){c.call(this,"expandAll",a)},collapseAll:function(a){c.call(this,"collapseAll",a)},destroy:function(a){c.call(this,"destroy",a)}},l=0,m=function(c,h){this.opts=a.extend(!0,{},a.fn.collapsable.defaults,h),this.items=[];var i=this;return this.uid=b(),this.promiseOpen=!1,d.call(this),g.call(this),c.each(function(){var a=new n(i,this);a.$box.length&&a.$control.length&&i.items.push(a)}),e.call(this),f.call(this),this};m.prototype.getExpanded=function(){for(var a=[],b=this.items.length,c=0;b>c;c++)this.items[c].isExpanded()&&a.push(c);return a},m.prototype.expandAll=function(b){if(!this.opts.grouped||!this.getExpanded().length)for(var c=a.Event("expandAll.collapsable"),d=this.items.length,e=0;d>e;e++)if(!this.items[e].isExpanded()){var f=this.items[e].expand(c,b);if(this.opts.grouped&&f)break}},m.prototype.collapseAll=function(b){for(var c=a.Event("collapseAll.collapsable"),d=this.getExpanded(),e=d.length,f=0;e>f;f++)this.items[d[f]].collapse(c,b)},m.prototype.destroy=function(){for(var b=this.opts,c=this.items.length,d=0;c>d;d++){var e=this.items[d];e.$collapsable.removeClass(b.classNames.collapsed+" "+b.classNames.expanded).removeData("collapsable").off(b.event+".collapsable"),e.$control.removeClass("ca-link"),e.$control.find(".ca-link").removeClass("ca-link"),e.$control.find("[data-ca-created]").each(function(){a(this).parent().html(a(this).html())});var f=e.$box.data("ca-pre-init-style")||"";e.$box.attr("style",f).removeData("ca-pre-init-style"),e.$collapsable.trigger("destroy.collapsable")}this.$extLinks.off("click.collapsable")};var n=function(b,c){if(this.parent=b,this.$collapsable=a(c),this.id=this.$collapsable.attr("id"),this.$control=this.$collapsable.find(b.opts.control),this.$box=this.$collapsable.find(b.opts.box),0!=this.$control.length&&0!=this.$box.length){var d=this,e=this.parent.opts;return h.call(this),this.$controlLink=this.$control.find(".ca-link"),this.$controlLink.on(e.event+".collapsable",function(a,b){var c=b?b:a;e.preventDefault&&a.preventDefault(),d.isExpanded()?d.collapse(c):d.expand(c)}),this.$collapsable.data("collapsable",this),this}};n.prototype.expand=function(b,c,d){var e=this.parent.opts,f=this.parent.getExpanded();if(this.parent.promiseOpen=!0,e.grouped&&f.length&&this.parent.items[f[0]].collapse(b,c,d)===!1)return this.parent.promiseOpen=!1,!1;this.parent.promiseOpen=!1;var g=a.Event("expand.collapsable",{customData:c,originalEvent:b});return this.$collapsable.trigger(g),g.isDefaultPrevented()&&!d?(!e.collapsableAll&&e.grouped&&this.parent.items[f[0]].expand(b,c,!0),!1):i.call(this,"expand",b)},n.prototype.collapse=function(b,c,d){var e=this.parent.opts;if(!e.collapsableAll&&!this.parent.promiseOpen&&this.parent.getExpanded().length<2)return!1;var f=a.Event("collapse.collapsable",{customData:c,originalEvent:b});return this.$collapsable.trigger(f),f.isDefaultPrevented()&&!d?!1:i.call(this,"collapse",b)},n.prototype.isExpanded=function(){return this.$collapsable.hasClass(this.parent.opts.classNames.expanded)},a.fn.collapsable=function(b){return k[b]?k[b].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof b&&b?void a.error("Method "+b+" does not exist on jQuery.collapsable"):(k.init.apply(this,arguments),this)},a.fn.collapsable.defaults=j}(jQuery); \ No newline at end of file diff --git a/jquery.collapsable.min.js b/jquery.collapsable.min.js deleted file mode 100644 index 2a50eaf..0000000 --- a/jquery.collapsable.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/* jquery.collapsable v2.0.0 | MIT License | Radek Šerý */ -!function(t){function e(){return"ca-uid-"+d++}function a(e,a){var l=[];this.each(function(){var s=t(this).data("collapsable");if(s){var i=s.parent.uid;-1===l.indexOf(i)&&(l.push(i),s.parent[e](a))}})}function l(){var e=this.opts;(this.$extLinks=t(e.extLinks.selector).filter("a")).length&&this.$extLinks.on("click.collapsable",function(a){e.extLinks.preventDefault&&a.preventDefault();var l=t(t(this).attr("href")).data("collapsable");l&&l.$controlLink.first().trigger(e.event+".collapsable",a)})}function s(){var e=window.location.href,a=e.search(/#/),l=-1,s=-1,i=t(t.map(this.items,function(t){return t.$collapsable.get()}));if(-1===a||(e=e.substring(a+1),s=i.index(t("#"+e)),-1===s||(this.items[s].defaultExpanded=!0,!this.opts.grouped))){if(this.opts.grouped){if(l=i.index(t("."+this.opts.classNames.defaultExpanded)),-1!==l)return void(this.items[l].defaultExpanded=!0)}else{var n=this;i.each(function(e){t(this).hasClass(n.opts.classNames.defaultExpanded)&&(l=e,n.items[e].defaultExpanded=!0)})}-1!==s||-1!==l||this.opts.collapsableAll||(this.items[0].defaultExpanded=!0)}}function i(){var e=t.Event("init.collapsable"),a=this.opts,l=this.items,s=a.fx;a.fx&&(a.fx="toggle");for(var i=l.length,n=!a.collapsableAll,o=0;i>o;o++)l[o].defaultExpanded?(l[o].expand(e,null,n),n=!1):l[o].collapse(e,null,!0);a.fx=s}function n(){var t=this.opts;("slide"===t.fx||"fade"===t.fx)&&(t.fxDuration=t.fxDuration||500),"slide"===t.fx?t.fx={expand:"slideDown",collapse:"slideUp"}:"fade"===this.opts.fx&&(t.fx={expand:"fadeIn",collapse:"fadeOut"})}function o(){var a=this;a.id||(a.id=e(),a.$collapsable.attr("id",this.id));var l=a.$box.attr("id")||a.id+"-ca-box";a.$box.attr("id",l).data("ca-pre-init-style",a.$box.attr("style")),a.$control.each(function(){var e,s=t(this);s.is("a")?e=s:(e=s.find("a")).length||(s.wrapInner(''),e=s.find("a")),e.addClass("ca-link"),e.attr("aria-controls",l)})}function r(e,a,l){var s=this.parent.opts,i=this,n="expanded",o=s.classNames.expanded,r=s.classNames.collapsed;"collapse"===e&&(n="collapsed",o=s.classNames.collapsed,r=s.classNames.expanded);var p=t.Event(n+".collapsable",{customData:l,originalEvent:a});return this.parent.$extLinks.filter("[href=#"+this.id+"]")["expand"===e?"addClass":"removeClass"](s.extLinks.activeClass),this.$collapsable.removeClass(r).addClass(o),this.$controlLink.attr("aria-expanded","expand"===e),this.$box.attr("aria-hidden","expand"!==e),"object"==typeof s.fx?this.$box[s.fx[e]](s.fxDuration,function(){i.$collapsable.trigger(p)}):("toggle"==s.fx&&this.$box["expand"===e?"show":"hide"](),setTimeout(function(){i.$collapsable.trigger(p)},s.fxDuration)),!0}var p={control:".ca-control",box:".ca-box",event:"click",fx:!1,fxDuration:0,grouped:!1,collapsableAll:!0,preventDefault:!0,extLinks:{selector:null,preventDefault:!1,activeClass:"ca-ext-active"},classNames:{expanded:"ca-expanded",collapsed:"ca-collapsed",defaultExpanded:"ca-default-expanded"}},c={init:function(t){return new h(this,t)},expandAll:function(t){a.call(this,"expandAll",t)},collapseAll:function(t){a.call(this,"collapseAll",t)},destroy:function(t){a.call(this,"destroy",t)}},d=0,h=function(a,o){this.opts=t.extend(!0,{},t.fn.collapsable.defaults,o),this.items=[];var r=this;return this.uid=e(),this.promiseOpen=!1,l.call(this),n.call(this),a.each(function(){var t=new f(r,this);t.$box.length&&t.$control.length&&r.items.push(t)}),s.call(this),i.call(this),this};h.prototype.getExpanded=function(){for(var t=[],e=this.items.length,a=0;e>a;a++)this.items[a].isExpanded()&&t.push(a);return t},h.prototype.expandAll=function(e){if(!this.opts.grouped||!this.getExpanded().length)for(var a=t.Event("expandAll.collapsable"),l=this.items.length,s=0;l>s;s++)if(!this.items[s].isExpanded()){var i=this.items[s].expand(a,e);if(this.opts.grouped&&i)break}},h.prototype.collapseAll=function(e){for(var a=t.Event("collapseAll.collapsable"),l=this.getExpanded(),s=l.length,i=0;s>i;i++)this.items[l[i]].collapse(a,e)},h.prototype.destroy=function(){for(var e=this.opts,a=this.items.length,l=0;a>l;l++){var s=this.items[l];s.$collapsable.removeClass(e.classNames.collapsed+" "+e.classNames.expanded).removeData("collapsable").off(e.event+".collapsable"),s.$control.removeClass("ca-link"),s.$control.find(".ca-link").removeClass("ca-link"),s.$control.find("[data-ca-created]").each(function(){t(this).parent().html(t(this).html())});var i=s.$box.data("ca-pre-init-style")||"";s.$box.attr("style",i).removeData("ca-pre-init-style"),s.$collapsable.trigger("destroy.collapsable")}this.$extLinks.off("click.collapsable")};var f=function(e,a){if(this.parent=e,this.$collapsable=t(a),this.id=this.$collapsable.attr("id"),this.$control=this.$collapsable.find(e.opts.control),this.$box=this.$collapsable.find(e.opts.box),0!=this.$control.length&&0!=this.$box.length){var l=this,s=this.parent.opts;return o.call(this),this.$controlLink=this.$control.find(".ca-link"),this.$controlLink.on(s.event+".collapsable",function(t,e){var a=e?e:t;s.preventDefault&&t.preventDefault(),l.isExpanded()?l.collapse(a):l.expand(a)}),this.$collapsable.data("collapsable",this),this}};f.prototype.expand=function(e,a,l){var s=this.parent.opts,i=this.parent.getExpanded();if(this.parent.promiseOpen=!0,s.grouped&&i.length&&this.parent.items[i[0]].collapse(e,a,l)===!1)return this.parent.promiseOpen=!1,!1;this.parent.promiseOpen=!1;var n=t.Event("expand.collapsable",{customData:a,originalEvent:e});return this.$collapsable.trigger(n),n.isDefaultPrevented()&&!l?(!s.collapsableAll&&s.grouped&&this.parent.items[i[0]].expand(e,a,!0),!1):r.call(this,"expand",e)},f.prototype.collapse=function(e,a,l){var s=this.parent.opts;if(!s.collapsableAll&&!this.parent.promiseOpen&&this.parent.getExpanded().length<2)return!1;var i=t.Event("collapse.collapsable",{customData:a,originalEvent:e});return this.$collapsable.trigger(i),i.isDefaultPrevented()&&!l?!1:r.call(this,"collapse",e)},f.prototype.isExpanded=function(){return this.$collapsable.hasClass(this.parent.opts.classNames.expanded)},t.fn.collapsable=function(e){return c[e]?c[e].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof e&&e?void t.error("Method "+e+" does not exist on jQuery.collapsable"):(c.init.apply(this,arguments),this)},t.fn.collapsable.defaults=p}(jQuery); diff --git a/package.json b/package.json new file mode 100644 index 0000000..6645f10 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "jquery.collapsable", + "title": "jQuery Collapsable", + "description": "jQuery plugin for collapsable boxes", + "version": "2.0.0", + "author": { + "name": "Radek Šerý", + "email": "radek.sery@peckadesign.cz" + }, + "repository": { + "type": "git", + "url": "https://github.com/peckadesign/jquery.collapsable" + }, + "keywords": ["jquery", "jquery-plugin", "collapsable", "jquery-collapsable"], + "bugs": "https://github.com/peckadesign/jquery.collapsable/issues", + "license": "MIT", + "homepage": "http://peckadesign.github.io/jquery.collapsable/", + "dependencies": {}, + "devDependencies": { + "grunt": "~0.4.5", + "grunt-cli": "~0.1.13", + "grunt-contrib-concat": "~1.0.0", + "grunt-contrib-uglify": "~0.6.0" + } +} diff --git a/src/jquery.collapsable.js b/src/jquery.collapsable.js new file mode 100644 index 0000000..2de95f1 --- /dev/null +++ b/src/jquery.collapsable.js @@ -0,0 +1,628 @@ +/** + * jquery.collapsable + */ +;(function($) { + + // @feature: díky předávání originalEvent do expand.collapsable (atd.) je možné použít e.originalEvent.preventDefault() místo defaults.preventDefault! cool, ne?! + + /** + * Collapsable defaults + * @type {{control: string, box: string, event: string, fx: boolean, fxDuration: number, grouped: 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 + + fx: false, // 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 + + grouped: 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 + preventDefault: true, // whether prevenDefault should be called when specified event occurs on control; even if false, e.originalEvent.preventDefault() may be used inside collapsable event handlers + + extLinks: { // external links for operating collapsable set, can be anywhere else in DOM + selector: null, // CSS selector for external links; it has to be anchors; the click event is binded + preventDefault: false, // whether preventDefault is called on extLinks click + activeClass: 'ca-ext-active' // class which would be toggled on external link when associated box is expanded or collapsed + }, + + 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' + } + + // 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 && grouped set + * to true) and limits amount of default expanded items to 1 when grouped 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.grouped) { + return; + } + } + } + + // max 1 expanded item + if (this.opts.grouped) { + defaultExpanded = $items.index($('.' + this.opts.classNames.defaultExpanded)); + + // max 1, we can return now + if (defaultExpanded !== -1) { + this.items[defaultExpanded].defaultExpanded = true; + return; + } + } + + // not grouped, 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.grouped, 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 || 500; + } + + // 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 grouped, we only want to expand one (first) box, or none if already expanded + if (this.opts.grouped && 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.grouped && 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') + .off(opts.event + '.collapsable'); + + // revert control element + item.$control.removeClass('ca-link'); + item.$control.find('.ca-link').removeClass('ca-link'); + item.$control.find('[data-ca-created]').each(function () { + $(this).parent().html($(this).html()); + }); + + // revert box element + var style = item.$box.data('ca-pre-init-style') || ''; + item.$box + .attr('style', style) + .removeData('ca-pre-init-style'); + + 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 = $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'); + + // originalEvent 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, originalEvent) { + var passEvent = originalEvent ? originalEvent : 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} originalEvent - Event to be passed to event handlers + * @returns {boolean} + */ + function handleExpandCollapse(action, originalEvent, 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, originalEvent: originalEvent }); + + // update extLinks + this.parent.$extLinks + .filter('[href=#' + this.id + ']') + [action === 'expand' ? 'addClass' : 'removeClass'](opts.extLinks.activeClass); + + // update classes on collapsable element itself + this.$collapsable + .removeClass(removeClass) + .addClass(addClass); + + // aria support + this.$controlLink.attr('aria-expanded', action === 'expand'); + this.$box.attr('aria-hidden', action !== 'expand'); + + // actually toggle the box state + if(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); + } + + return true; + } + + /** + * Expands single CollapsableItem; could be prevented by `preventDefault` called on `expand.collapsable` event + * @param {Object} originalEvent - 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(originalEvent, data, force) { + var opts = this.parent.opts; + var expandedItem = this.parent.getExpanded(); // grouped -> max one expanded item + + this.parent.promiseOpen = true; // allows us to collapse expanded item even if there might be collapseAll === false option + if (opts.grouped) { + // before expanding, we have to collapse previously opened item, if grouped element hasn't collapsed, we can't continue + if (expandedItem.length && this.parent.items[expandedItem[0]].collapse(originalEvent, data, force) === false) { + this.parent.promiseOpen = false; + return false; + } + } + this.parent.promiseOpen = false; + + var event = $.Event('expand.collapsable', { customData: data, originalEvent: originalEvent }); + this.$collapsable.trigger(event); + + if (event.isDefaultPrevented() && ! force) { + // collapsableAll === false && grouped === true -> if the box has not opened, we must make sure something is opened, therefore we force-open previously opened box (opts.grouped is true means we tried to collapse something), simulating it has never closed in first place + if (! opts.collapsableAll && opts.grouped) { + this.parent.items[expandedItem[0]].expand(originalEvent, data, true); + } + + return false; + } + + return handleExpandCollapse.call(this, 'expand', originalEvent) + }; + + /** + * Collapses single CollapsableItem; could be prevented by `preventDefault` called on `collapse.collapsable` event + * @param {Object} originalEvent - 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(originalEvent, 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, originalEvent: originalEvent }); + this.$collapsable.trigger(event); + + if (event.isDefaultPrevented() && !force) { + return false; + } + + return handleExpandCollapse.call(this, 'collapse', originalEvent); + }; + + /** + * Tests if the CollapsableItem is expanded or not + * @returns {Boolean} + */ + CollapsableItem.prototype.isExpanded = function() { + return this.$collapsable.hasClass(this.parent.opts.classNames.expanded); + }; + + + /** + * 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); From 6a48405fef99b46fe877622b0f2bc0f1088fc995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20=C5=A0er=C3=BD?= Date: Thu, 3 Mar 2016 22:15:05 +0100 Subject: [PATCH 10/10] #0 license and readme update --- LICENSE | 2 +- README.md | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/LICENSE b/LICENSE index b3aad60..f37485a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 PeckaDesign +Copyright (c) 2016 Radek Šerý Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b46cf70..d053b6d 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,21 @@ -# jquery.collapsable +# jQuery Collapsable -## Instalace - -Do závislostí projektu je možné plugin přidat přes odkaz na Github následovně (více viz: http://bower.io/docs/creating-packages/#dependencies): +## Instalation +tba ``` $ cat bower.json { - "name": "Projekt", + "name": "Project", "private": true, "dependencies": { - "jquery.collapsable": "peckadesign/jquery.collapsable#1.0.*" + "jquery.collapsable": "peckadesign/jquery.collapsable#2.0.*" } } ``` ## Changelog -### v1.1.0 +### 2.0.0 -- přejmenování `fxSpeed` na `collapseDelay`, nově lze použít i bez `fx`, v tu chvíli nastavuje zpoždění mezi `onExpand/onCollapse` a `onExpanded/onCollapsed` -- oprava výchozího otevírání boxů při načtení stránky dle class a kotvy v url -- úprava callbacků při kliku na `ca-control` prvky; místo `return false;` nově `e.preventDefault()`, díky tomu lze načítat obsah boxů ajaxem (nezablokuje odeslání požadavku) -- přidána podpora pd extension `spinner` (pokud je) +- Plugin has been completely rewritten.