// vim: et:ts=4:sw=4:sts=4:ft=javascript /*@ Ox.Menu Menu Object () -> Menu Object (options) -> Menu Object (options, self) -> Menu Object options Options object 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 left left top top parent the supermenu, if any selected the position of the selected item side open to 'bottom' or 'right' size 'large', 'medium' or 'small' self shared private variable 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) { self = self || {}; var that = Ox.Element({}, self) .defaults({ element: null, id: '', items: [], mainmenu: null, maxWidth: 0, offset: { left: 0, top: 0 }, parent: null, selected: -1, side: 'bottom', size: 'medium' // fixme: remove }) .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); 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 (self.options.parent) { self.options.parent.hideMenu(true).triggerEvent('click'); } if (item.options('checked') !== null) { if (item.options('group')) { toggled = self.optionGroups[item.options('group')].toggle(position); if (toggled.length) { toggled.forEach(function(pos) { that.items[pos].toggleChecked(); }); menu.triggerEvent('change', { id: item.options('group'), checked: self.optionGroups[item.options('group')].checked().map(function(v) { return { id: that.items[v].options('id'), title: Ox.isString(that.items[v].options('title')[0]) ? Ox.stripTags(that.items[v].options('title')[0]) : '' }; }) }); } } else { item.toggleChecked(); menu.triggerEvent('change', { checked: item.options('checked'), id: item.options('id'), title: Ox.isString(item.options('title')[0]) ? 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 clickLayer() { that.hideMenu(); } 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] = item.items.map(function(v, i) { return Ox.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(Ox.MenuItem(Ox.extend(item, { maxWidth: self.options.maxWidth, 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] = 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) { // fixme: this exists in ox.js by now 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 && 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.isString(item.options('title')[0]) ? 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.setOption = function(key, value) { if (key == 'items') { constructItems(value); } else if (key == 'selected') { that.$content.find('.OxSelected').removeClass('OxSelected'); selectItem(value); } } /*@ addItem @*/ that.addItem = function(item, position) { }; /*@ addItemAfter @*/ that.addItemAfter = function(item, id) { }; /*@ addItemBefore @*/ that.addItemBefore = function(item, id) { }; /*@ checkItem @*/ that.checkItem = function(id) { Ox.print('checkItem id', id) var ids = id.split('_'), item; if (ids.length == 1) { item = that.getItem(id); Ox.print('checkItem', id, item, that.submenus) 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 }); } } else { that.submenus[ids.shift()].checkItem(ids.join('_')); } }; /*@ getItem @*/ that.getItem = function(id) { //Ox.print('getItem 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; }; /*@ getSubmenu @*/ 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; } /*@ hasEnabledItems @*/ that.hasEnabledItems = function() { var ret = false; Ox.forEach(that.items, function(item) { if (!item.options('disabled')) { return ret = true; } }); return ret; }; /*@ hideMenu () -> Menu Object @*/ that.hideMenu = function(hideParent) { 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 }); hideParent && self.options.parent.hideMenu(true); } that.$layer && that.$layer.hide(); that.hide().loseFocus().triggerEvent('hide'); return that; }; /*@ removeItem removeItem @*/ that.removeItem = function() { }; /*@ selectFirstItem selectFirstItem @*/ that.selectFirstItem = function() { selectNextItem(); }; /*@ showMenu showMenu () -> Menu Object @*/ that.showMenu = function() { if (!that.is(':hidden')) { return; } 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 = Ox.Layer({type: 'menu'}) .css({top: self.options.mainmenu ? '20px' : 0}) .bindEvent({click: clickLayer}) .show(); } return that; //that.triggerEvent('show'); }; /*@ toggleMenu toggleMenu @*/ that.toggleMenu = function() { return that.is(':hidden') ? that.showMenu() : that.hideMenu(); }; return that; };