/* NAME: MDSelectMenu.js AUTHOR: Ben Delaney DESCRIPTION: Creates a powerful, hierarchical replacement for the traditional HTML select element. REQUIREMENTS: 1) A JSON object (see below). 2) CSS - SelectMenu.css - The CSS in this file is *very* specific, so it's not recommmended to change anything inside that file (other than image filepaths obviously). 3) Supporting imagery - Contained in the SelectMenuImages folder (these can exist elsewhere, but the paths in SelectMenu.css need to be changed). 4) A Container element. This is just a block-level element into which both the TRIGGER and the MENU will be injected (see example code below). PROVIDES: MD.SelectMenu OPTIONS: RETURNS: HOW TO USE: // Data { menu: [ { text: 'test1' ,value: '1' } ,{ text: 'test2' ,value: '2' } ] } OR [ { text: 'test1' ,value: '1' } ,{ text: 'test2' ,value: '2' } ] OR ['test', 'test1', 'test2'] // Instantiation var selectMenu = new MD.SelectMenu({ attachTo: [element] // (optional) the element to attach the menu to }); // Attaching to elements selectMenu.attachTo($('one')); // causes $('one') to become the trigger for the menu // If the open, close, and toggle methods are used independently, they need to pass the attachTo element each time. $('one').addEvent('click', function(){ selectMenu.toggle(this); // OR selectMenu.open(this); // OR selectMenu.close(this); }); EXAMPLE: // in case you need these... { "menu": { "id": "abc123" ,"menuItems": [ { "text": "Selection Name" ,"id":"111" ,"action": "alert('do something')" } ] } }; { "menu": { "id": "abc123" ,"menuItems": [ { "text": "Selection Name" ,"id":"111" ,"action": "alert('do something')" ,"menuItems": [ { "text": "Selection Name" ,"id":"222" ,"action": "alert('do something')" } ,{ "text": "Selection Name" ,"id":"333" ,"action": "alert('do something')" } ] } ,{ "text": "Selection Name" ,"id":"444" ,"action": "alert('do something')" ,"menuItems": [ { "text": "Selection Name" ,"id":"555" ,"action": "alert('do something')" } ,{ "text": "Selection Name" ,"id":"666" ,"action": "alert('do something')" } ] } ] } }; */ var testData = [ { text: 'test1' ,value: '1' // ,tomato: function() {console.error('yo');} ,properties: { title: 'a title tag' ,rel: 'whatever' } } ,{ text: 'test2' ,value: '2' // ,selected: true // ,action: function() {console.error('oiwejf');} } ,{ text: 'test3' ,value: '3' ,subMenu: [ { text: 'sub1' ,value: 's1' ,subMenu: [ { text: 'sub1_a' ,value: 's1_a' } ,{ text: 'sub2_a' ,value: 's2_a' } ] } ,{ text: 'sub2' ,value: 's2' } ] } ,{ text: 'test4' ,value: '4' } ]; MD.SelectMenu = new Class({ Implements: [Options,Events] ,options: { data: null ,attachTo: null ,closeOnSelect: true ,blinkSelectedItem: true ,highlightSelectedItem: true ,injectInto: 'body' // [string or element] ,menuItemClassName: 'MenuItemText' // Items ,itemsMenuRootKey: null // if the 'data' property is an object, we need to know the key of the array containing the root items ,itemTextKey: null // The name of the property that contains the text for each item. ,itemValueKey: null // The name of the property that contains the value for each item. ,itemSubMenuKey: null // The name of the property that contains the submenu for each item. ,itemActionKey: null // The name of the property that contains the optional action/function that is triggered when the user clicks the item. Ignored if the menu item has a submenu. ,itemStringMod: null // [function] ,itemMod: null // [function] takes an 'item' [obj] as a parameter and alters it // Menu Element(s) ,className: '' ,properties: {} ,position: 'bottomLeft' ,edge: 'topLeft' ,offset: {} ,flyoutDirection: 'right' ,styles: {} // Events // ,onSelect: function(selectedItem){} // The action to apply to every item that is clicked. // ,onBlur: function(thisObj){} // ,onOpen: function(thisObj) {} // ,onClose: function(thisObj) {} } ,initialize: function(options){ this.setOptions(options); this.data = this.options.data; // Determine the nature of the items data and set it up if (this.data) { if (typeOf(this.data == 'array')) { this.items = this.data; } else if (typeOf(this.data == 'object')) { if (!this.options.itemsMenuRootKey) { throw new Error('No itemsMenuRootKey supplied for this instance of MD.SelectMenu.'); return; } else { this.items = this.data[this.options.itemsMenuRootKey]; } } else { throw new Error('The data supplied to this instance of MD.SelectMenu does not appear to be formatted correctly.'); console.error('BAD DATA:', this.data); return; } } else { // console.log('No data supplied to this instance of MD.SelectMenu', this); return; } // Loop through the JSON and set the selected value and selected text IF present this.items.each(function(item, index, array) { if ((item.selected && item.selected == true) || (this.value && this.value == (item.value || item[this.options.itemValueKey]))) { this.selectedText = (item.text || item[this.options.itemTextKey]); this.value = (item.value || item[this.options.itemValueKey]); } }, this); if (this.options.attachTo) { this.attachTo($(this.options.attachTo)); } } ,attachTo: function(element){ element.setStyle('cursor', 'pointer'); this.attachToElement = element; this.attachToElement.addEvents({ click: function(){ if (this.isOpen()) { this.close(); } else { this.open(this.attachToElement); } }.bind(this) }); return this; } ,open: function(attachToElement){ this.build(); this.menuKeyboardEvents = new Keyboard({ eventType: 'keydown' ,events: { down: function(e){ e.preventDefault(); this.focusNextMenuItem(); }.bind(this) ,up: function(e){ e.preventDefault(); this.focusPreviousMenuItem(); }.bind(this) ,enter: function(e){ e.preventDefault(); if (this.focusedItem != '') this.selectItem(this.focusedItem); this.menuKeyboardEvents.deactivate(); }.bind(this) ,space: function(e){ e.preventDefault(); if (this.focusedItem != '') this.selectItem(this.focusedItem); this.menuKeyboardEvents.deactivate(); }.bind(this) ,esc: function(e){ e.preventDefault(); this.close(); }.bind(this) } }).activate(); this.html.inject((this.options.injectInto == 'body' ? $$('body')[0] : (this.options.injectInto ? $(this.options.injectInto) : $$('body')[0]))).show(); this.position($(attachToElement)); attachToElement.addClass('MDSelectMenu-Open'); // Add the clickout event (function(){ this.html.addEvent('clickout', function(){ this.close(); }.bind(this)); }.bind(this)).delay(50); this.openState = true; this.fireEvent('open', this, 50); return this; } ,position: function(relativeToElement) { var htmlHeight = this.html.getSize().y; var relativeToElementCoords = (relativeToElement || $(this.options.attachTo) || this.attachToElement).getCoordinates(); var windowCoords = window.getCoordinates(); var offset = this.options.offset; var availableHeight = (windowCoords.bottom - relativeToElementCoords.bottom) - 14; if (htmlHeight - (windowCoords.bottom - relativeToElementCoords.bottom) > 0) { offset.y = -(htmlHeight - (windowCoords.bottom - relativeToElementCoords.bottom) + 6); this.html.setStyles({ 'max-height': window.getSize().y - 14 ,overflow: 'auto' }); } this.html.position({ relativeTo: (relativeToElement || $(this.options.attachTo) || this.attachToElement) ,position: this.options.position ,edge: this.options.edge ,offset: offset }); } ,isOpen: function(){ return this.openState; } ,build: function() { if ($(this.html)) { this.html.destroy(); delete this.html; } this.html = new Element('ul', Object.merge(this.options.properties, { 'class': 'MDSelectMenu '+this.options.flyoutDirection.toLowerCase()+' '+this.options.className ,styles: this.options.styles })); // Loop through the items and create their elements this.items.each(function(item, index, array) { this.html.grab( item.element = this.buildItem(item) ); }, this); } ,buildItem: function(item){ if (this.options.itemStringMod) { item.originalText = item[this.options.itemTextKey]; var changedText = function(){ return this.options.itemStringMod(item[this.options.itemTextKey]); }.bind(this); item[this.options.itemTextKey] = changedText(); } item.text = (item.text || item[this.options.itemTextKey]); item.properties = item.properties || {}; item.element = new Element('li.MDSelectMenu-Item', Object.merge({ events:{ click: function(e){ e.stopPropagation(); if ( ! item.deactivated) { if (this.options.itemActionKey && item[this.options.itemActionKey]) { item[this.options.itemActionKey].call(); } else if (item.action) { item.action.call(); } this.selectItem(item); } }.bind(this) ,mouseenter: function(e){ this.getParent().getChildren().removeClass('Over'); this.addClass('Over'); } ,mouseleave: function(e){ this.removeClass('Over'); } } }, item.properties)).grab( item.innerElement = new Element('a.MenuItem').grab( item.textElement = new Element('span', { html: item.text, 'class': this.options.menuItemClassName }) ) ); if (item.deactivated) { item.element.addClass('Deactivated'); } if (this.options.itemMod) this.options.itemMod.call(this, item); // Deal with subMenu(s) recursively... var subMenuItems = (item.subMenu || item[this.options.itemSubMenuKey]); if (subMenuItems) { item.element.adopt( new Element('div', { 'class': 'SubMenuArrow' ,'html': '>' }) ,item.subMenuElement = new Element('ul.MDSelectMenu-SubMenu') ); subMenuItems.each(function(subitem) { item.subMenuElement.grab( subitem.element = this.buildItem(subitem) ); }, this); } if ((this.value && this.value == (item.value || item[this.options.itemValueKey])) || (!this.value && item.selected && item.selected == true)) { if (this.options.highlightSelectedItem) item.element.addClass('Selected'); this.selectedText = (item.text || item[this.options.itemTextKey]); } // if (this.value && this.value == (item.value || item[this.options.itemValueKey])) item.element.addClass('Selected'); return item.element; } ,close: function(){ if (this.isOpen()) this.html.destroy(); this.openState = false; if (this.menuKeyboardEvents) this.menuKeyboardEvents.deactivate(); if(this.attachToElement)this.attachToElement.removeClass('MDSelectMenu-Open'); this.fireEvent('close', this, 50); return this; } ,focusOnItem: function(item){ item.element.addClass('Over'); this.focusedItem = item; } ,focusNextMenuItem: function(){ if (!this.focusedItem) { this.focusOnItem(this.items[0]); } else { this.items.each(function(item) { item.element.removeClass('Over'); }); var i = this.items.indexOf(this.focusedItem); if (i+1 != this.items.length) { this.focusOnItem(this.items[i+1]); } else { this.focusOnItem(this.items[i]); } } } ,focusPreviousMenuItem: function(){ if (!this.focusedItem) { this.focusOnItem(this.items[0]); } else { this.items.each(function(item) { item.element.removeClass('Over'); }); var i = this.items.indexOf(this.focusedItem); if (i == 0) { this.focusOnItem(this.items[i]); } else { this.focusOnItem(this.items[i-1]); } } } ,selectItem: function(item, hidden){ if (!this.html) this.build(); var item; switch (typeOf(item)){ case 'string': this.items.each(function(i) { if (i.text == item) item = i; }); break; case 'object': item = item; break; case 'number': this.items.each(function(i, index) { if (item == index) item = i; }); break; default: console.error('Cannot select item - not a valid type.', item); } this.items.each(function(i) {i.element.removeClass('Selected');}); this.value = item.value = (item.value || item[this.options.itemValueKey]); this.selectedText = (item.text || item[this.options.itemTextKey]); if (!hidden) { if (this.options.blinkSelected) { console.log('should be blinking'); console.log('item.element', item.element); item.element.addClass('Selected'); (function(){ item.element.removeClass('Selected'); }).delay(100); (function(){ item.element.addClass('Selected'); }).delay(200); if (!this.options.highlightSelected) (function(){ item.element.removeClass('Selected'); }).delay(300); } if (this.options.highlightSelected) { (function(){ item.element.addClass('Selected'); }).delay((this.options.blinkSelected ? 300 : 0)); } } this.fireEvent('select', item); if (!hidden) {if (this.options.closeOnSelect) this.close();} } ,getItemBy: function(property, value) { var item; this.items.some(function(it) { if (it[property] == value) { item = it; return true; } return false; }); return item; } ,getItemByValue: function(value) { return this.getItemBy('value', value); } ,deactivateItem: function(item) { item.deactivated = true; } ,activateItem: function(item) { item.deactivated = false; } ,getValue: function(){ return this.value; } ,setValue: function(value) { // Loop through the JSON and set the selected value and selected text IF present this.items.each(function(item, index, array) { if (value && value == (item.value || item[this.options.itemValueKey])) { this.selectedText = (item.text || item[this.options.itemTextKey]); this.value = (item.value || item[this.options.itemValueKey]); } }, this); } ,clearSelected: function(){ var clearThis = function(item){ item.element.removeClass('Selected'); var subMenuItems = (item.subMenu || item[this.options.itemSubMenuKey]); if (subMenuItems) { subMenuItems.each(function(subitem) { clearThis(subitem); }); } }.bind(this); this.items.each(function(item) { clearThis(item); }, this); this.value = ''; return this; } ,toElement: function(){ return this.html; } });