oxjs/source/Ox.UI/js/Menu/Ox.Menu.js

736 lines
24 KiB
JavaScript
Raw Normal View History

2011-07-29 18:48:43 +00:00
// vim: et:ts=4:sw=4:sts=4:ft=javascript
2011-05-16 08:24:46 +00:00
/*@
Ox.Menu <f:Ox.Element> Menu Object
() -> <f> Menu Object
(options) -> <f> Menu Object
(options, self) -> <f> Menu Object
options <o> Options object
element <o> the element the menu is attached to
id <s> the menu id
items <a> array of menu items
mainmenu <o> the main menu this menu is part of, if any
offset <o> offset of the menu, in px
left <n> left
top <n> top
parent <o> the supermenu, if any
selected <b> the position of the selected item
side <s> open to 'bottom' or 'right'
size <s> 'large', 'medium' or 'small'
self <o> 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
@*/
2011-04-22 22:03:10 +00:00
Ox.Menu = function(options, self) {
2011-08-09 17:00:39 +00:00
self = self || {};
var that = Ox.Element({}, self)
2011-04-22 22:03:10 +00:00
.defaults({
element: null,
id: '',
items: [],
mainmenu: null,
offset: {
left: 0,
top: 0
},
parent: null,
selected: -1,
side: 'bottom',
size: 'medium' // fixme: remove
2011-04-22 22:03:10 +00:00
})
.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 = $('<div>')
.addClass('OxTop')
.appendTo(that.$element);
that.$scrollbars.up = constructScrollbar('up')
.appendTo(that.$element);
that.$container = $('<div>')
.addClass('OxContainer')
.appendTo(that.$element);
that.$content = $('<table>')
.addClass('OxContent')
.appendTo(that.$container);
constructItems(self.options.items);
that.$scrollbars.down = constructScrollbar('down')
.appendTo(that.$element);
that.$bottom = $('<div>')
.addClass('OxBottom')
.appendTo(that.$element);
that.$layer = $('<div>')
.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) {
2011-09-17 23:25:22 +00:00
if (self.options.parent) {
self.options.parent.hideMenu(true).triggerEvent('click');
2011-04-22 22:03:10 +00:00
}
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) {
2011-04-22 22:03:10 +00:00
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] = item.items.map(function(v, i) {
return Ox.extend(v, {
2011-04-22 22:03:10 +00:00
group: item.group
});
});
2011-06-19 18:25:37 +00:00
self.optionGroups[item.group] = new Ox.OptionGroup(
2011-04-22 22:03:10 +00:00
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, {
2011-04-22 22:03:10 +00:00
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({
2011-04-22 22:03:10 +00:00
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 $('<tr>').append(
$('<td>', {
'class': 'OxLine',
colspan: 5
})
);
}
function constructScrollbar(direction) {
var interval,
speed = direction == 'up' ? -1 : 1;
return $('<div/>', {
'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 $('<tr>').append(
$('<td>', {'class': 'OxSpace', colspan: 5})
2011-04-22 22:03:10 +00:00
);
}
function getElement(id) {
// fixme: needed?
return $('#' + Ox.toCamelCase(options.id + '/' + id));
}
function getItemPositionById(id) {
2011-09-05 01:30:35 +00:00
// fixme: this exists in ox.js by now
2011-04-22 22:03:10 +00:00
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]
2011-05-20 11:46:52 +00:00
item && item.removeClass('OxSelected');
2011-04-22 22:03:10 +00:00
/* 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');
2011-05-20 11:46:52 +00:00
///* disabled
2011-04-22 22:03:10 +00:00
that.triggerEvent('select', {
id: item.options('id'),
title: Ox.stripTags(item.options('title')[0])
});
2011-05-20 11:46:52 +00:00
//*/
2011-04-22 22:03:10 +00:00
}
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();
}
}
2011-04-29 12:40:51 +00:00
self.setOption = function(key, value) {
2011-04-22 22:03:10 +00:00
if (key == 'items') {
constructItems(value);
} else if (key == 'selected') {
that.$content.find('.OxSelected').removeClass('OxSelected');
selectItem(value);
}
}
2011-05-16 08:24:46 +00:00
/*@
addItem <f>
@*/
2011-04-22 22:03:10 +00:00
that.addItem = function(item, position) {
};
2011-05-16 08:24:46 +00:00
/*@
addItemAfter <f>
@*/
2011-04-22 22:03:10 +00:00
that.addItemAfter = function(item, id) {
};
2011-05-16 08:24:46 +00:00
/*@
addItemBefore <f>
@*/
2011-04-22 22:03:10 +00:00
that.addItemBefore = function(item, id) {
};
2011-05-16 08:24:46 +00:00
/*@
checkItem <f>
@*/
2011-04-22 22:03:10 +00:00
that.checkItem = function(id) {
2011-09-17 23:25:22 +00:00
//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
2011-04-22 22:03:10 +00:00
});
}
} else {
2011-09-17 23:25:22 +00:00
that.submenus[ids.shift()].checkItem(ids.join('_'));
2011-04-22 22:03:10 +00:00
}
};
2011-05-16 08:24:46 +00:00
/*@
getItem <f>
@*/
2011-04-22 22:03:10 +00:00
that.getItem = function(id) {
2011-09-17 23:25:22 +00:00
//Ox.print('getItem id', id)
2011-04-22 22:03:10 +00:00
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;
};
2011-05-16 08:24:46 +00:00
/*@
getSubmenu <f>
@*/
2011-04-22 22:03:10 +00:00
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;
}
2011-05-16 08:24:46 +00:00
/*@
hasEnabledItems <f>
@*/
2011-04-22 22:03:10 +00:00
that.hasEnabledItems = function() {
var ret = false;
Ox.forEach(that.items, function(item) {
if (!item.options('disabled')) {
return ret = true;
}
});
return ret;
};
2011-05-16 08:24:46 +00:00
/*@
hideMenu <f>
() -> <f> Menu Object
@*/
2011-09-17 23:25:22 +00:00
that.hideMenu = function(hideParent) {
2011-04-22 22:03:10 +00:00
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
});
2011-09-17 23:25:22 +00:00
hideParent && self.options.parent.hideMenu(true);
2011-04-22 22:03:10 +00:00
}
2011-09-17 23:25:22 +00:00
that.hide().loseFocus().triggerEvent('hide');
2011-04-22 22:03:10 +00:00
that.$layer.hide();
return that;
};
2011-05-16 08:24:46 +00:00
/*@
2011-05-16 10:49:48 +00:00
removeItem <f> removeItem
2011-05-16 08:24:46 +00:00
@*/
2011-04-22 22:03:10 +00:00
that.removeItem = function() {
};
2011-05-16 08:24:46 +00:00
/*@
2011-05-16 10:49:48 +00:00
selectFirstItem <f> selectFirstItem
2011-05-16 08:24:46 +00:00
@*/
2011-04-22 22:03:10 +00:00
that.selectFirstItem = function() {
selectNextItem();
};
2011-05-16 08:24:46 +00:00
/*@
2011-05-16 10:49:48 +00:00
showMenu <f> showMenu
2011-05-16 08:24:46 +00:00
() -> <f> Menu Object
@*/
2011-04-22 22:03:10 +00:00
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');
};
2011-05-16 08:24:46 +00:00
/*@
toggleMenu <f> toggleMenu
2011-05-16 08:24:46 +00:00
@*/
2011-04-22 22:03:10 +00:00
that.toggleMenu = function() {
return that.is(':hidden') ? that.showMenu() : that.hideMenu();
2011-04-22 22:03:10 +00:00
};
return that;
};