/*
* toggle.js
*
* dependencies: prototype.js, lowpro.js, effect.js
*
* --------------------------------------------------------------------------
*
* A LowPro and Prototype-based library with a collection of behaviors for
* unobtrusively toggling the visibility of other elements via links,
* checkboxes, radio buttons, and selects.
*
* To use you will need to install the following LowPro behaviors. If you are
* using Rails, put this in "application.js":
*
* Event.addBehavior({
* 'a.toggle': Toggle.LinkBehavior(),
* 'input.checkbox.toggle': Toggle.CheckboxBehavior(),
* 'div.radio_group.toggle': Toggle.RadioGroupBehavior(),
* 'select.toggle': Toggle.SelectBehavior()
* });
*
* Once the hooks are installed correctly, you should add a "rel" attribute
* to each element that you want to use as a toggle trigger. Set the value
* of the "rel" attribute to "toggle[id]" where id is equal to the ID of
* the element that you want to toggle. You can toggle multiple elements by
* separating the IDs with commas (like this: "toggle[id1,id2,id3]").
*
* For example, a link with a class of "toggle":
*
* More
*
* will become a trigger for a div with an ID of "more". Checkboxes work in
* the exact same manner. To use with a group of radio buttons, make sure
* that all of the radio buttons are inside of a div with a class of
* "radio_group toggle". Then set the "rel" attribute on each radio button
* that should act as a toggle trigger. Selects work in a similar manner,
* but the "rel" attribute should be set on each option element that should
* toggle the visibility of an element or array of elements.
*
* Each of the included LowPro behaviors can be customized in various ways.
* Check out the inline documentation for detailed usage information.
*
* Project Homepage: http://github.com/fivepointssolutions/togglejs
*
* --------------------------------------------------------------------------
*
* Copyright (c) 2007-2010, Five Points Solutions, Inc.
* Copyright (c) 2010, John W. Long
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
var Toggle = {
DefaultEffect: 'slide',
DefaultEffectDuration: 0.25,
EffectPairs: {
'slide' : ['SlideDown','SlideUp'],
'blind' : ['BlindDown','BlindUp'],
'appear': ['Appear','Fade']
},
/**
* Toggle.extractAnchor(url) -> String
*
* Utility function. Returns everything after the first "#" character in a
* string. Used to extract the anchor from a URL.
**/
extractAnchor: function(url) {
var matches = String(url).match(/\#(.+)$/);
if (matches) return matches[1];
},
/**
* Toggle.extractToggleObjects(string) -> Array
*
* Utility function. Returns the associated toggle elements in a string. For
* string "toggle[one,two,three]" it will return the elements with IDs of
* "one", "two", and "three".
**/
extractToggleObjects: function(string) {
var matches = String(string).match(/^toggle\[(.+)\]$/);
if (matches) {
var ids = matches[1].split(',');
var elements = [];
ids.each(function(id) { elements.push($(id)) });
return elements;
} else {
return [];
}
},
/**
* Toggle.toggle(elements, effect, options)
*
* Utility function. Toggles an element or array of elements with effect
* and options. Similar to `Effect.toggle()`, but works with multiple
* elements and also supports setting effect to "none".
*
* Parameters
* - elements: An element or array of elements to toggle
* - effect: This option specifies the effect that should be used when
* toggling. The default is "slide", but it can also be set to
* "blind", "appear", or "none".
* - options: The standard Effect options hash with the addition of
* beforeToggle and afterToggle events.
**/
toggle: function(elements, effect, options) {
var elements = $A([elements]).flatten();
var effect = (effect || Toggle.DefaultEffect).toLowerCase();
var options = options || {};
if (effect == 'none') {
if (options.beforeStart) options.beforeStart();
elements.invoke("toggle");
if (options.afterFinish) options.afterFinish();
} else {
options.duration = options.duration || Toggle.DefaultEffectDuration;
var effects = elements.map(function(e) {
var element = $(e);
var inOrOut = element.visible() ? 1 : 0;
var name = Toggle.EffectPairs[effect][inOrOut];
return new Effect[name](element, { sync: true });
});
new Effect.Parallel(effects, options);
}
},
/**
* Toggle.show(elements, effect, options)
*
* Utility function. Shows an element or array of elements with effect
* and options.
**/
show: function(elements, effect, options) {
var elements = $([elements]).flatten();
elements = elements.map(function(element) { return $(element) });
elements = elements.reject(function(element) { return element.visible() });
Toggle.toggle(elements, effect, options);
},
/**
* Toggle.hide(elements, effect, options)
*
* Utility function. Hides an element or array of elements with effect
* and options.
**/
hide: function(elements, effect, options) {
var elements = $([elements]).flatten();
elements = elements.map(function(element) { return $(element) });
elements = elements.reject(function(element) { return !element.visible() });
Toggle.toggle(elements, effect, options);
},
/**
* Toggle.wrapElement(element)
*
* Utility function. Wraps element with a div of class "toggle_wrapper"
* unless one already exists. Returns the "toggle_wrapper" for given
* element. This is necessary because effects only work properly on
* elements that do not have padding, borders, or margin.
**/
wrapElement: function(element) {
var element = $(element);
var parent = $(element.parentNode);
if (parent.hasClassName('toggle_wrapper')) {
return parent;
} else {
return element.wrap($div({'class': 'toggle_wrapper', 'style': 'display: none'}));
}
}
};
/**
* class Toggle.LinkBehavior < Behavior
*
* Allows a link to toggle the display of another element or array of
* elements on and off. Just set the rel attribute to
* "toggle[id1,id2,...]" on the link and the href of the link to the
* ID of the first element ("#id1").
*
* Options
* - effect: This option specifies the effect that should be used when
* toggling. The default is "slide", but it can also be set to
* "blind", "appear", or "none".
* - onLoad: Called after the behavior is initialized. The function is
* automatically bound to the behavior (so "this" referes to the
* behavior).
* - beforeToggle: Called after the link is clicked, but before the effect is
* started. The link is passed as the first parameter and the
* function is automatically bound to the behavior (so "this"
* refers to the behavior).
* - afterToggle: Called after the effect is complete. The link is passed as
* the first parameter and the function is automatically bound
* to the behavior (so "this" refers to the behavior).
**/
Toggle.LinkBehavior = Behavior.create({
initialize: function(options) {
var options = options || {};
this.effect = options.effect || Toggle.DefaultEffect;
this.onLoad = options.onLoad || Prototype.emptyFunction;
this.onLoad.bind(this);
this.beforeToggle = options.beforeToggle || Prototype.emptyFunction;
this.beforeToggle.bind(this);
this.afterToggle = options.afterToggle || Prototype.emptyFunction;
this.afterToggle.bind(this);
var elements = Toggle.extractToggleObjects(this.element.readAttribute('rel'));
this.toggleWrappers = elements.map(function(e) { return Toggle.wrapElement(e) });
this.toggleID = Toggle.extractAnchor(this.element.href);
this.element.behavior = this; // a bit of a hack
Toggle.addLink(this.toggleID, this.element);
this.onLoad(this.element);
},
onclick: function() {
this.toggle();
return false;
},
toggle: function() {
Toggle.toggle(
this.toggleWrappers,
this.effect,
{
beforeStart: function() { this.beforeToggle(this.element) }.bind(this),
afterFinish: function() { this.afterToggle(this.element) }.bind(this)
}
);
}
});
Toggle.links = {};
Toggle.addLink = function(id, element) {
this.links[id] = this.links[id] || $A();
this.links[id].push(element);
};
// Automatically toggle associated element if anchor is equal to the ID of the
// link's associated element.
Event.observe(window, 'dom:loaded', function() {
var anchor = Toggle.extractAnchor(window.location);
var links = Toggle.links[anchor];
if (links) {
var behavior = links.first().behavior;
behavior.onclick();
}
});
/**
* class Toggle.CheckboxBehavior < Behavior
*
* Allows a the selection of a checkbox to toggle an element or group of
* elements on and off. Just set the `rel` attribute to "toggle[id1,id2,...]"
* on the checkbox.
*
* Options
* - invert: When set to true the associated element is hidden when checked.
* - effect: This option specifies the effect that should be used when
* toggling. The default is "slide", but it can also be set to
* "blind", "appear", or "none".
**/
Toggle.CheckboxBehavior = Behavior.create({
initialize: function(options) {
var options = options || {};
this.invert = options.invert;
var elements = Toggle.extractToggleObjects(this.element.readAttribute('rel'));
this.toggleWrappers = elements.map(function(e) { return Toggle.wrapElement(e) });
this.effect = 'none';
this.toggle();
this.effect = options.effect || Toggle.DefaultEffect;
},
onclick: function(event) {
this.toggle();
},
toggle: function() {
var method, formElementMethod;
if (this.invert) {
method = this.element.checked ? 'hide' : 'show';
formElementMethod = this.element.checked ? 'disable' : 'enable';
} else {
method = this.element.checked ? 'show' : 'hide';
formElementMethod = this.element.checked ? 'enable' : 'disable';
}
Toggle[method](this.toggleWrappers, this.effect);
// Disable/enable form elements based on whether the container is
// visible or not.
this.toggleWrappers.each(function(wrapper) {
Form.getElements(wrapper).invoke(formElementMethod);
});
}
});
/**
* class Toggle.RadioGroupBehavior < Behavior
*
* Allows you to toggle elements based on the selection of a group of radio
* buttons. Just set the rel attribute to "toggle[id1,id2,...]" on
* each radio button. Radio buttons must be grouped inside a containing
* element to which the behavior is applied.
*
* Options
* - effect: This option specifies the effect that should be used when
* toggling. The default is "slide", but it can also be set to
* "blind", "appear", or "none".
**/
Toggle.RadioGroupBehavior = Behavior.create({
initialize: function(options) {
var options = options || {};
this.radioButtons = this.element.select('input[type=radio]');
this.toggleWrapperIDs = $A();
this.toggleWrapperIDsFor = {};
this.radioButtons.each(function(radioButton) {
var elements = Toggle.extractToggleObjects(radioButton.readAttribute('rel'))
var ids = elements.invoke('identify');
var wrapperIDs = elements.map(function(e) { return Toggle.wrapElement(e) }).invoke('identify');
this.toggleWrapperIDsFor[radioButton.identify()] = wrapperIDs;
this.toggleWrapperIDs.push(wrapperIDs);
radioButton.observe('click', this.onRadioButtonClick.bind(this));
}.bind(this));
this.toggleWrapperIDs = this.toggleWrapperIDs.flatten().uniq()
this.effect = "none";
this.toggle();
this.effect = options.effect || Toggle.DefaultEffect;
},
onRadioButtonClick: function(event) {
this.toggle();
},
toggle: function() {
var group = this.element;
var radioButton = this.radioButtons.find(function(b) { return b.checked });
var wrapperIDs = this.toggleWrapperIDsFor[radioButton.identify()];
var partitioned = this.toggleWrapperIDs.partition(function(id) { return wrapperIDs.include(id) });
Toggle.show(partitioned[0], this.effect);
Toggle.hide(partitioned[1], this.effect);
}
});
/**
* class Toggle.SelectBehavior < Behavior
*
* Allows you to toggle elements based on the selection of a combo box. Just
* set the rel attribute to "toggle[id1,id2,...]" on the each select
* option.
*
* Options
* - effect: This option specifies the effect that should be used when
* toggling. The default is "slide", but it can also be set to
* "blind", "appear", or "none".
**/
Toggle.SelectBehavior = Behavior.create({
initialize: function(options) {
var options = options || {};
var optionElements = this.element.select('option');
this.toggleWrapperIDs = $A();
this.toggleWrapperIDsFor = {};
optionElements.each(function(optionElement) {
var elements = Toggle.extractToggleObjects(optionElement.readAttribute('rel'))
var wrapperIDs = elements.map(function(e) { return Toggle.wrapElement(e) }).invoke('identify');
this.toggleWrapperIDsFor[optionElement.identify()] = wrapperIDs;
this.toggleWrapperIDs.push(wrapperIDs);
}.bind(this));
this.toggleWrapperIDs = this.toggleWrapperIDs.flatten().uniq()
this.effect = "none";
this.toggle();
this.effect = options.effect || Toggle.DefaultEffect;
},
onchange: function(event) {
this.toggle();
},
toggle: function() {
var combo = this.element;
var option = $(combo.options[combo.selectedIndex]);
var wrapperIDs = this.toggleWrapperIDsFor[option.identify()];
var partitioned = this.toggleWrapperIDs.partition(function(id) { return wrapperIDs.include(id) });
Toggle.show(partitioned[0], this.effect);
Toggle.hide(partitioned[1], this.effect);
}
});