/** options element the element the menu is attached to id the menu id items array of menu items mainmenu the main menu this menu is part of, if any offset offset of the menu, in px parent the supermenu, if any selected the position of the selected item side open to 'bottom' or 'right' size 'large', 'medium' or 'small' events: change_groupId {id, value} checked item of a group has changed click_itemId item not belonging to a group was clicked click_menuId {id, value} item not belonging to a group was clicked deselect_menuId {id, value} item was deselected not needed, not implemented hide_menuId menu was hidden select_menuId {id, value} item was selected */ Ox.Menu = function(options, self) { var self = self || {}, that = new Ox.Element({}, self) .defaults({ element: null, id: '', items: [], mainmenu: null, offset: { left: 0, top: 0 }, parent: null, selected: -1, side: 'bottom', size: 'medium', }) .options(options || {}) .addClass( 'OxMenu Ox' + Ox.toTitleCase(self.options.side) + ' Ox' + Ox.toTitleCase(self.options.size) ) .click(click) .mouseenter(mouseenter) .mouseleave(mouseleave) .mousemove(mousemove) .bindEvent({ key_up: selectPreviousItem, key_down: selectNextItem, key_left: selectSupermenu, key_right: selectSubmenu, key_escape: hideMenu, key_enter: clickSelectedItem }), itemHeight = self.options.size == 'small' ? 12 : (self.options.size == 'medium' ? 16 : 20), // menuHeight, scrollSpeed = 1, $item; // fixme: used? // fixme: attach all private vars to self // construct that.items = []; that.submenus = {}; that.$scrollbars = []; that.$top = $('
') .addClass('OxTop') .appendTo(that.$element); that.$scrollbars.up = constructScrollbar('up') .appendTo(that.$element); that.$container = $('
') .addClass('OxContainer') .appendTo(that.$element); that.$content = $('') .addClass('OxContent') .appendTo(that.$container); constructItems(self.options.items); that.$scrollbars.down = constructScrollbar('down') .appendTo(that.$element); that.$bottom = $('
') .addClass('OxBottom') .appendTo(that.$element); that.$layer = $('
') .addClass(self.options.mainmenu ? 'OxMainMenuLayer' : 'OxMenuLayer') .click(click); function click(event) { var item, position, $target = $(event.target), $parent = $target.parent(); // necessary for highlight if ($parent.is('.OxCell')) { $target = $parent; $parent = $target.parent(); } if ($target.is('.OxCell')) { position = $parent.data('position'); item = that.items[position]; if (!item.options('disabled')) { clickItem(position); } else { that.hideMenu(); } } else { that.hideMenu(); } } function clickItem(position) { var item = that.items[position], menu = self.options.mainmenu || self.options.parent || that, toggled; that.hideMenu(); if (!item.options('items').length) { if (that.options('parent')) { that.options('parent').hideMenu().triggerEvent('click'); } if (item.options('checked') !== null) { if (item.options('group')) { //Ox.print('has group', item.options('group')) toggled = self.optionGroups[item.options('group')].toggle(position); //Ox.print('toggled', toggled) if (toggled.length) { toggled.forEach(function(pos) { that.items[pos].toggleChecked(); }); //Ox.print('--triggering change event--'); menu.triggerEvent('change', { id: item.options('group'), checked: $.map(self.optionGroups[item.options('group')].checked(), function(v, i) { return { id: that.items[v].options('id'), title: Ox.stripTags(that.items[v].options('title')[0]) }; }) }); } } else { item.toggleChecked(); menu.triggerEvent('change', { checked: item.options('checked'), id: item.options('id'), title: Ox.stripTags(item.options('title')[0]) }); } } else { menu.triggerEvent('click', { id: item.options('id'), title: Ox.stripTags(item.options('title')[0]) }); } if (item.options('title').length == 2) { item.toggleTitle(); } } } function clickSelectedItem() { // called on key.enter if (self.options.selected > -1) { clickItem(self.options.selected); } else { that.hideMenu(); } } function constructItems(items) { that.$content.empty(); scrollMenuUp(); self.optionGroups = {}; items.forEach(function(item, i) { if (item.group) { items[i] = $.map(item.items, function(v, i) { return $.extend(v, { group: item.group }); }); self.optionGroups[item.group] = new Ox.OptionGroup( items[i], 'min' in item ? item.min : 1, 'max' in item ? item.max : 1 ); } }); items = Ox.flatten(items); that.items = []; items.forEach(function(item) { var position; if ('id' in item) { that.items.push(new Ox.MenuItem($.extend(item, { menu: that, position: position = that.items.length })).data('position', position).appendTo(that.$content)); // fixme: jquery bug when passing {position: position}? does not return the object?; if (item.items) { that.submenus[item.id] = new Ox.Menu({ element: that.items[position], id: Ox.toCamelCase(self.options.id + '/' + item.id), items: item.items, mainmenu: self.options.mainmenu, offset: { left: 0, top: -4 }, parent: that, side: 'right', size: self.options.size, }); } } else { that.$content.append(constructSpace()); that.$content.append(constructLine()); that.$content.append(constructSpace()); } }); if (!that.is(':hidden')) { that.hideMenu(); that.showMenu(); } } function constructLine() { return $('
').append( $('').append( $('
', { 'class': 'OxLine', colspan: 5 }) ); } function constructScrollbar(direction) { var interval, speed = direction == 'up' ? -1 : 1; return $('
', { 'class': 'OxScrollbar Ox' + Ox.toTitleCase(direction), html: Ox.UI.symbols['triangle_' + direction], click: function() { // fixme: do we need to listen to click event? return false; }, mousedown: function() { scrollSpeed = 2; return false; }, mouseenter: function() { var $otherScrollbar = that.$scrollbars[direction == 'up' ? 'down' : 'up']; $(this).addClass('OxSelected'); if ($otherScrollbar.is(':hidden')) { $otherScrollbar.show(); that.$container.height(that.$container.height() - itemHeight); if (direction == 'down') { that.$content.css({ top: -itemHeight + 'px' }); } } scrollMenu(speed); interval = setInterval(function() { scrollMenu(speed); }, 100); }, mouseleave: function() { $(this).removeClass('OxSelected'); clearInterval(interval); }, mouseup: function() { scrollSpeed = 1; return false; } }); } function constructSpace() { return $('
', { 'class': 'OxSpace', colspan: 5 }) ); } function getElement(id) { // fixme: needed? return $('#' + Ox.toCamelCase(options.id + '/' + id)); } function getItemPositionById(id) { var position; Ox.forEach(that.items, function(item, i) { if (item.options('id') == id) { position = i; return false; } }); return position; } function hideMenu() { // called on key_escape that.hideMenu(); } function isFirstEnabledItem() { var ret = true; Ox.forEach(that.items, function(item, i) { if (i < self.options.selected && !item.options('disabled')) { return ret = false; } }); return ret; } function isLastEnabledItem() { var ret = true; Ox.forEach(that.items, function(item, i) { if (i > self.options.selected && !item.options('disabled')) { return ret = false; } }); return ret; } function mouseenter() { that.gainFocus(); } function mouseleave() { if (self.options.selected > -1 && !that.items[self.options.selected].options('items').length) { selectItem(-1); } } function mousemove(event) { var item, position, $target = $(event.target); $parent = $target.parent(); if ($parent.is('.OxCell')) { $target = $parent; $parent = $target.parent(); } if ($target.is('.OxCell')) { position = $parent.data('position'); item = that.items[position]; if (!item.options('disabled') && position != self.options.selected) { selectItem(position); } } else { mouseleave(); } } function scrollMenu(speed) { var containerHeight = that.$container.height(), contentHeight = that.$content.height(), top = parseInt(that.$content.css('top')) || 0, min = containerHeight - contentHeight + itemHeight, max = 0; top += speed * scrollSpeed * -itemHeight; if (top <= min) { top = min; that.$scrollbars.down.hide().trigger('mouseleave'); that.$container.height(containerHeight + itemHeight); that.items[that.items.length - 1].trigger('mouseover'); } else if (top >= max - itemHeight) { top = max; that.$scrollbars.up.hide().trigger('mouseleave'); that.$container.height(containerHeight + itemHeight); that.items[0].trigger('mouseover'); } that.$content.css({ top: top + 'px' }); } function scrollMenuUp() { if (that.$scrollbars.up.is(':visible')) { that.$content.css({ top: '0px' }); that.$scrollbars.up.hide(); if (that.$scrollbars.down.is(':hidden')) { that.$scrollbars.down.show(); } else { that.$container.height(that.$container.height() + itemHeight); } } } function selectItem(position) { var item; if (self.options.selected > -1) { //Ox.print('s.o.s', self.options.selected, that.items) item = that.items[self.options.selected] item.removeClass('OxSelected'); /* disabled that.triggerEvent('deselect', { id: item.options('id'), title: Ox.stripTags(item.options('title')[0]) }); */ } if (position > -1) { item = that.items[position]; Ox.forEach(that.submenus, function(submenu, id) { if (!submenu.is(':hidden')) { submenu.hideMenu(); return false; } }); item.options('items').length && that.submenus[item.options('id')].showMenu(); // fixme: do we want to switch to this style? item.addClass('OxSelected'); /* disabled that.triggerEvent('select', { id: item.options('id'), title: Ox.stripTags(item.options('title')[0]) }); */ } self.options.selected = position; } function selectNextItem() { var offset, selected = self.options.selected; //Ox.print('sNI', selected) if (!isLastEnabledItem()) { if (selected == -1) { scrollMenuUp(); } else { that.items[selected].removeClass('OxSelected'); } do { selected++; } while (that.items[selected].options('disabled')) selectItem(selected); offset = that.items[selected].offset().top + itemHeight - that.$container.offset().top - that.$container.height(); if (offset > 0) { if (that.$scrollbars.up.is(':hidden')) { that.$scrollbars.up.show(); that.$container.height(that.$container.height() - itemHeight); offset += itemHeight; } if (selected == that.items.length - 1) { that.$scrollbars.down.hide(); that.$container.height(that.$container.height() + itemHeight); } else { that.$content.css({ top: ((parseInt(that.$content.css('top')) || 0) - offset) + 'px' }); } } } } function selectPreviousItem() { var offset, selected = self.options.selected; //Ox.print('sPI', selected) if (selected > - 1) { if (!isFirstEnabledItem()) { that.items[selected].removeClass('OxSelected'); do { selected--; } while (that.items[selected].options('disabled')) selectItem(selected); } offset = that.items[selected].offset().top - that.$container.offset().top; if (offset < 0) { if (that.$scrollbars.down.is(':hidden')) { that.$scrollbars.down.show(); that.$container.height(that.$container.height() - itemHeight); } if (selected == 0) { that.$scrollbars.up.hide(); that.$container.height(that.$container.height() + itemHeight); } that.$content.css({ top: ((parseInt(that.$content.css('top')) || 0) - offset) + 'px' }); } } } function selectSubmenu() { //Ox.print('selectSubmenu', self.options.selected) if (self.options.selected > -1) { var submenu = that.submenus[that.items[self.options.selected].options('id')]; //Ox.print('submenu', submenu, that.submenus); if (submenu && submenu.hasEnabledItems()) { submenu.gainFocus(); submenu.selectFirstItem(); } else if (self.options.mainmenu) { self.options.mainmenu.selectNextMenu(); } } else if (self.options.mainmenu) { self.options.mainmenu.selectNextMenu(); } } function selectSupermenu() { //Ox.print('selectSupermenu', self.options.selected) if (self.options.parent) { self.options.selected > -1 && that.items[self.options.selected].trigger('mouseleave'); scrollMenuUp(); self.options.parent.gainFocus(); } else if (self.options.mainmenu) { self.options.mainmenu.selectPreviousMenu(); } } self.onChange = function(key, value) { if (key == 'items') { constructItems(value); } else if (key == 'selected') { that.$content.find('.OxSelected').removeClass('OxSelected'); selectItem(value); } } that.addItem = function(item, position) { }; that.addItemAfter = function(item, id) { }; that.addItemBefore = function(item, id) { }; that.checkItem = function(id) { var item = that.getItem(id); if (item.options('group')) { var position = getItemPositionById(id), toggled = self.optionGroups[item.options('group')].toggle(position); if (toggled.length) { toggled.forEach(function(pos) { that.items[pos].toggleChecked(); }); } } else { item.options({ checked: true }); } }; that.getItem = function(id) { //Ox.print('id', id) var ids = id.split('_'), item; if (ids.length == 1) { Ox.forEach(that.items, function(v) { if (v.options('id') == id) { item = v; return false; } }); if (!item) { Ox.forEach(that.submenus, function(submenu) { item = submenu.getItem(id); return !item; }); } } else { item = that.submenus[ids.shift()].getItem(ids.join('_')); } return item; }; that.getSubmenu = function(id) { var ids = id.split('_'), submenu; if (ids.length == 1) { submenu = that.submenus[id]; } else { submenu = that.submenus[ids.shift()].getSubmenu(ids.join('_')); } //Ox.print('getSubmenu', id, submenu); return submenu; } that.hasEnabledItems = function() { var ret = false; Ox.forEach(that.items, function(item) { if (!item.options('disabled')) { return ret = true; } }); return ret; }; that.hideMenu = function() { if (that.is(':hidden')) { return; } Ox.forEach(that.submenus, function(submenu) { if (submenu.is(':visible')) { submenu.hideMenu(); return false; } }); selectItem(-1); scrollMenuUp(); that.$scrollbars.up.is(':visible') && that.$scrollbars.up.hide(); that.$scrollbars.down.is(':visible') && that.$scrollbars.down.hide(); if (self.options.parent) { //self.options.element.removeClass('OxSelected'); self.options.parent.options({ selected: -1 }); } that.hide() .loseFocus() .triggerEvent('hide'); that.$layer.hide(); return that; }; that.removeItem = function() { }; that.selectFirstItem = function() { selectNextItem(); }; that.showMenu = function() { if (!that.is(':hidden')) { return; } if (!self.options.parent && !that.$layer.parent().length) { that.$layer.appendTo(Ox.UI.$body); } that.parent().length == 0 && that.appendTo(Ox.UI.$body); that.css({ left: '-1000px', top: '-1000px', }).show(); var offset = self.options.element.offset(), width = self.options.element.outerWidth(), height = self.options.element.outerHeight(), left = Ox.limit( offset.left + self.options.offset.left + (self.options.side == 'bottom' ? 0 : width), 0, Ox.UI.$window.width() - that.width() ), top = offset.top + self.options.offset.top + (self.options.side == 'bottom' ? height : 0), menuHeight = that.$content.outerHeight(); // fixme: why is outerHeight 0 when hidden? menuMaxHeight = Math.floor(Ox.UI.$window.height() - top - 16); if (self.options.parent) { if (menuHeight > menuMaxHeight) { top = Ox.limit(top - menuHeight + menuMaxHeight, self.options.parent.offset().top, top); menuMaxHeight = Math.floor(Ox.UI.$window.height() - top - 16); } } that.css({ left: left + 'px', top: top + 'px' }); if (menuHeight > menuMaxHeight) { that.$container.height(menuMaxHeight - itemHeight - 8); // margin that.$scrollbars.down.show(); } else { that.$container.height(menuHeight); } if (!self.options.parent) { that.gainFocus(); } that.$layer.show(); return that; //that.triggerEvent('show'); }; that.toggleMenu = function() { that.is(':hidden') ? that.showMenu() : that.hideMenu(); }; return that; };