themes/unitexgramlab/js/language-dropdown.js
/*
Dropdown.js
Creates touch-friendly drop-down menus
Created by Stephen Morley - http://code.stephenmorley.org/ - and released under
the terms of the CC0 1.0 Universal legal code:
http://creativecommons.org/publicdomain/zero/1.0/legalcode
changes made by martinec:
- use classie (https://github.com/ded/bonzo) to handle class operations
*/
// create the Dropdown object
var Dropdown = (function(){
// the delay, in milliseconds
var DELAY = 250;
// the list of menus
var menus = [];
// Initialises the drop-down menus.
function initialise(){
// listen for touch events on the document if appropriate
if ('createTouch' in document){
document.body.addEventListener('touchstart', handleTouchStart, false);
}
// loop over the menus, converting them
var menus = document.querySelectorAll('ul.dropdown');
for (var i = 0; i < menus.length; i ++) applyTo(menus[i]);
}
/* Handles a touch start event. The parameter is:
*
* e - the event
*/
function handleTouchStart(e){
// determine whether any menu is open
var isOpen = false;
for (var i = 0; i < menus.length; i ++){
if (menus[i].isOpen) isOpen = true;
}
// return immediately if all menus are closed
if (!isOpen) return;
// move up the document tree until we reach the root node
var node = e.target;
while (node != null){
// return immediately if we are inside a drop-down menu
if (classie.has( node, 'dropdown' )) return;
// move onto the parent node
node = node.parentNode;
}
// close all menus
close();
}
/* Closes all menus except the specified menu. The parameter is:
*
* menu - a menu not to close; this parameter is optional
*/
function close(menu){
// loop over the menus, closing them
for (var i = 0; i < menus.length; i ++){
if (menus[i] != menu) menus[i].close();
}
}
/* Creates a drop-down menu. The parameter is:
*
* node - either the DOM node of the menu or the ID of the node
*/
function applyTo(node){
// fetch the DOM node if a string was supplied
if (typeof node == 'string') node = document.getElementById(node);
// create and store the new menu
menus.push(new Menu(node));
}
/* Creates a drop-down menu. The parameter is:
*
* node - the DOM node of the menu
*/
function Menu(node){
// store the node
this.node = node;
// update the class name
classie.add( node, 'dropdownJavaScript' );
// listen for mouse events
if ('addEventListener' in node){
node.addEventListener(
'mouseover', this.bind(this.handleMouseOver), false);
node.addEventListener('mouseout', this.bind(this.handleMouseOut), false);
node.addEventListener('click', this.bind(this.handleClick), false);
}else{
node.attachEvent('onmouseover', this.bind(this.handleMouseOver));
node.attachEvent('onmouseout', this.bind(this.handleMouseOut));
node.attachEvent('onclick', this.bind(this.handleClick));
}
// listen for touch events if appropriate
if ('createTouch' in document){
node.addEventListener('touchstart', this.bind(this.handleClick), false);
}
}
// whether the menu is open
Menu.prototype.isOpen = false;
// the timeout
Menu.prototype.timeout = null;
/* Binds the specified function to the current object. The parameter is:
*
* f - the function
*/
Menu.prototype.bind = function(f){
// return the bound function
var thisObject = this;
return function(){ f.apply(thisObject, arguments); }
}
/* Handles a mouse over event. The parameters are:
*
* e - the event
* immediate - true to open the menu without a delay
*/
Menu.prototype.handleMouseOver = function(e, immediate){
// clear the timeout
this.clearTimeout();
// find the parent list item
var item = ('target' in e ? e.target : e.srcElement);
while (item.nodeName != 'LI' && item != this.node) item = item.parentNode;
// if the target is within a list item, set the timeout
if (item.nodeName == 'LI'){
this.toOpen = item;
this.timeout =
window.setTimeout(this.bind(this.open), (immediate ? 0 : DELAY));
}
}
// Handles a mouse out event.
Menu.prototype.handleMouseOut = function(){
// clear the timeout
this.clearTimeout();
// set the timeout
this.timeout = window.setTimeout(this.bind(this.close), DELAY);
}
/* Handles a click event. The parameter is:
*
* e - the event
*/
Menu.prototype.handleClick = function(e){
// close any other menus
close(this);
// find the parent list item
var item = ('target' in e ? e.target : e.srcElement);
while (item.nodeName != 'LI' && item != this.node) item = item.parentNode;
// check that the target is within a list item
if (item.nodeName == 'LI'){
// check whether the item has a closed submenu
var submenu = this.getChildrenByTagName(item, 'UL');
if (submenu.length > 0 && !classie.has( item, 'dropdownOpen' )){
// open the submenu
this.handleMouseOver(e, true);
// prevent the default action
if ('preventDefault' in e){
e.preventDefault();
}else{
e.returnValue = false;
}
}
}
}
// Clears the timeout.
Menu.prototype.clearTimeout = function(){
// clear the timeout
if (this.timeout){
window.clearTimeout(this.timeout);
this.timeout = null;
}
}
// Opens the last item hovered over.
Menu.prototype.open = function(){
// store that the menu is open
this.isOpen = true;
// loop over the list items with the same parent
var items = this.getChildrenByTagName(this.toOpen.parentNode, 'LI');
for (var i = 0; i < items.length; i ++){
// check whether there is a submenu
var submenu = this.getChildrenByTagName(items[i], 'UL');
if (submenu.length > 0){
// check whether the submenu should be opened or closed
if (items[i] != this.toOpen){
// close the submenu
classie.remove( items[i], 'dropdownOpen' );
this.close(items[i]);
}else if (!classie.has( items[i], 'dropdownOpen' )){
// open the submenu
classie.add( items[i], 'dropdownOpen' );
// determine the location of the edges of the submenu
var left = 0;
var node = submenu[0];
while (node){
left += node.offsetLeft;
node = node.offsetParent;
}
right = left + submenu[0].offsetWidth;
// move the submenu to the right of the item if appropriate
if (left < 0) classie.add( items[i], 'dropdownLeftToRight' );
// move the submenu to the left of the item if appropriate
if (right > document.body.clientWidth){
classie.add( items[i], 'dropdownRightToLeft' );
}
}
}
}
}
/* Closes the menus within the specified node. The parameter is:
*
* node - the node; if omitted, all menus are closed
*/
Menu.prototype.close = function(node){
// if no node was specified, close all menus
if (!node){
this.isOpen = false;
node = this.node;
}
// loop over the items, closing their submenus
var items = node.getElementsByTagName('li');
for (var i = 0; i < items.length; i ++){
classie.remove( items[i], 'dropdownOpen' );
}
}
/* Returns an array containing the children of the specified node with the
* specified tag name. The parameters are:
*
* node - the node
* tagName - the tag name
*/
Menu.prototype.getChildrenByTagName = function(node, tagName){
// initialise the list of children
var result = [];
// loop over the children, adding those with the right tag name to the list
for (var i = 0; i < node.childNodes.length; i ++){
if (node.childNodes[i].nodeName == tagName){
result.push(node.childNodes[i]);
}
}
// return the children
return result;
}
// return the public API
return {
initialise : initialise,
applyTo : applyTo
};
})();