/** * DropMenu.js * * Copyright 2009, Moxiecode Systems AB * Released under LGPL License. * * License: http://tinymce.moxiecode.com/license * Contributing: http://tinymce.moxiecode.com/contributing */ (function(tinymce) { var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element; /** * This class is used to create drop menus, a drop menu can be a * context menu, or a menu for a list box or a menu bar. * * @class tinymce.ui.DropMenu * @extends tinymce.ui.Menu */ tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', { /** * Constructs a new drop menu control instance. * * @constructor * @method DropMenu * @param {String} id Button control id for the button. * @param {Object} s Optional name/value settings object. */ DropMenu : function(id, s) { s = s || {}; s.container = s.container || DOM.doc.body; s.offset_x = s.offset_x || 0; s.offset_y = s.offset_y || 0; s.vp_offset_x = s.vp_offset_x || 0; s.vp_offset_y = s.vp_offset_y || 0; if (is(s.icons) && !s.icons) s['class'] += ' mceNoIcons'; this.parent(id, s); this.onShowMenu = new tinymce.util.Dispatcher(this); this.onHideMenu = new tinymce.util.Dispatcher(this); this.classPrefix = 'mceMenu'; }, /** * Created a new sub menu for the drop menu control. * * @method createMenu * @param {Object} s Optional name/value settings object. * @return {tinymce.ui.DropMenu} New drop menu instance. */ createMenu : function(s) { var t = this, cs = t.settings, m; s.container = s.container || cs.container; s.parent = t; s.constrain = s.constrain || cs.constrain; s['class'] = s['class'] || cs['class']; s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x; s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y; s.keyboard_focus = cs.keyboard_focus; m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s); m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem); return m; }, focus : function() { var t = this; if (t.keyboardNav) { t.keyboardNav.focus(); } }, /** * Repaints the menu after new items have been added dynamically. * * @method update */ update : function() { var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th; tw = s.max_width ? Math.min(tb.clientWidth, s.max_width) : tb.clientWidth; th = s.max_height ? Math.min(tb.clientHeight, s.max_height) : tb.clientHeight; if (!DOM.boxModel) t.element.setStyles({width : tw + 2, height : th + 2}); else t.element.setStyles({width : tw, height : th}); if (s.max_width) DOM.setStyle(co, 'width', tw); if (s.max_height) { DOM.setStyle(co, 'height', th); if (tb.clientHeight < s.max_height) DOM.setStyle(co, 'overflow', 'hidden'); } }, /** * Displays the menu at the specified cordinate. * * @method showMenu * @param {Number} x Horizontal position of the menu. * @param {Number} y Vertical position of the menu. * @param {Numner} px Optional parent X position used when menus are cascading. */ showMenu : function(x, y, px) { var t = this, s = t.settings, co, vp = DOM.getViewPort(), w, h, mx, my, ot = 2, dm, tb, cp = t.classPrefix; t.collapse(1); if (t.isMenuVisible) return; if (!t.rendered) { co = DOM.add(t.settings.container, t.renderNode()); each(t.items, function(o) { o.postRender(); }); t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); } else co = DOM.get('menu_' + t.id); // Move layer out of sight unless it's Opera since it scrolls to top of page due to an bug if (!tinymce.isOpera) DOM.setStyles(co, {left : -0xFFFF , top : -0xFFFF}); DOM.show(co); t.update(); x += s.offset_x || 0; y += s.offset_y || 0; vp.w -= 4; vp.h -= 4; // Move inside viewport if not submenu if (s.constrain) { w = co.clientWidth - ot; h = co.clientHeight - ot; mx = vp.x + vp.w; my = vp.y + vp.h; if ((x + s.vp_offset_x + w) > mx) x = px ? px - w : Math.max(0, (mx - s.vp_offset_x) - w); if ((y + s.vp_offset_y + h) > my) y = Math.max(0, (my - s.vp_offset_y) - h); } DOM.setStyles(co, {left : x , top : y}); t.element.update(); t.isMenuVisible = 1; t.mouseClickFunc = Event.add(co, 'click', function(e) { var m; e = e.target; if (e && (e = DOM.getParent(e, 'tr')) && !DOM.hasClass(e, cp + 'ItemSub')) { m = t.items[e.id]; if (m.isDisabled()) return; dm = t; while (dm) { if (dm.hideMenu) dm.hideMenu(); dm = dm.settings.parent; } if (m.settings.onclick) m.settings.onclick(e); return Event.cancel(e); // Cancel to fix onbeforeunload problem } }); if (t.hasMenus()) { t.mouseOverFunc = Event.add(co, 'mouseover', function(e) { var m, r, mi; e = e.target; if (e && (e = DOM.getParent(e, 'tr'))) { m = t.items[e.id]; if (t.lastMenu) t.lastMenu.collapse(1); if (m.isDisabled()) return; if (e && DOM.hasClass(e, cp + 'ItemSub')) { //p = DOM.getPos(s.container); r = DOM.getRect(e); m.showMenu((r.x + r.w - ot), r.y - ot, r.x); t.lastMenu = m; DOM.addClass(DOM.get(m.id).firstChild, cp + 'ItemActive'); } } }); } Event.add(co, 'keydown', t._keyHandler, t); t.onShowMenu.dispatch(t); if (s.keyboard_focus) { t._setupKeyboardNav(); } }, /** * Hides the displayed menu. * * @method hideMenu */ hideMenu : function(c) { var t = this, co = DOM.get('menu_' + t.id), e; if (!t.isMenuVisible) return; if (t.keyboardNav) t.keyboardNav.destroy(); Event.remove(co, 'mouseover', t.mouseOverFunc); Event.remove(co, 'click', t.mouseClickFunc); Event.remove(co, 'keydown', t._keyHandler); DOM.hide(co); t.isMenuVisible = 0; if (!c) t.collapse(1); if (t.element) t.element.hide(); if (e = DOM.get(t.id)) DOM.removeClass(e.firstChild, t.classPrefix + 'ItemActive'); t.onHideMenu.dispatch(t); }, /** * Adds a new menu, menu item or sub classes of them to the drop menu. * * @method add * @param {tinymce.ui.Control} o Menu or menu item to add to the drop menu. * @return {tinymce.ui.Control} Same as the input control, the menu or menu item. */ add : function(o) { var t = this, co; o = t.parent(o); if (t.isRendered && (co = DOM.get('menu_' + t.id))) t._add(DOM.select('tbody', co)[0], o); return o; }, /** * Collapses the menu, this will hide the menu and all menu items. * * @method collapse * @param {Boolean} d Optional deep state. If this is set to true all children will be collapsed as well. */ collapse : function(d) { this.parent(d); this.hideMenu(1); }, /** * Removes a specific sub menu or menu item from the drop menu. * * @method remove * @param {tinymce.ui.Control} o Menu item or menu to remove from drop menu. * @return {tinymce.ui.Control} Control instance or null if it wasn't found. */ remove : function(o) { DOM.remove(o.id); this.destroy(); return this.parent(o); }, /** * Destroys the menu. This will remove the menu from the DOM and any events added to it etc. * * @method destroy */ destroy : function() { var t = this, co = DOM.get('menu_' + t.id); if (t.keyboardNav) t.keyboardNav.destroy(); Event.remove(co, 'mouseover', t.mouseOverFunc); Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc); Event.remove(co, 'click', t.mouseClickFunc); Event.remove(co, 'keydown', t._keyHandler); if (t.element) t.element.remove(); DOM.remove(co); }, /** * Renders the specified menu node to the dom. * * @method renderNode * @return {Element} Container element for the drop menu. */ renderNode : function() { var t = this, s = t.settings, n, tb, co, w; w = DOM.create('div', {role: 'listbox', id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000;outline:0'}); if (t.settings.parent) { DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id); } co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')}); t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); if (s.menu_line) DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'}); // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'}); n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0}); tb = DOM.add(n, 'tbody'); each(t.items, function(o) { t._add(tb, o); }); t.rendered = true; return w; }, // Internal functions _setupKeyboardNav : function(){ var contextMenu, menuItems, t=this; contextMenu = DOM.select('#menu_' + t.id)[0]; menuItems = DOM.select('a[role=option]', 'menu_' + t.id); menuItems.splice(0,0,contextMenu); t.keyboardNav = new tinymce.ui.KeyboardNavigation({ root: 'menu_' + t.id, items: menuItems, onCancel: function() { t.hideMenu(); }, enableUpDown: true }); contextMenu.focus(); }, _keyHandler : function(evt) { var t = this, e; switch (evt.keyCode) { case 37: // Left if (t.settings.parent) { t.hideMenu(); t.settings.parent.focus(); Event.cancel(evt); } break; case 39: // Right if (t.mouseOverFunc) t.mouseOverFunc(evt); break; } }, _add : function(tb, o) { var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic; if (s.separator) { ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'}); DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'}); if (n = ro.previousSibling) DOM.addClass(n, 'mceLast'); return; } n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'}); n = it = DOM.add(n, s.titleItem ? 'th' : 'td'); n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'}); if (s.parent) { DOM.setAttrib(a, 'aria-haspopup', 'true'); DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id); } DOM.addClass(it, s['class']); // n = DOM.add(n, 'span', {'class' : 'item'}); ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')}); if (s.icon_src) DOM.add(ic, 'img', {src : s.icon_src}); n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title); if (o.settings.style) DOM.setAttrib(n, 'style', o.settings.style); if (tb.childNodes.length == 1) DOM.addClass(ro, 'mceFirst'); if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator')) DOM.addClass(ro, 'mceFirst'); if (o.collapse) DOM.addClass(ro, cp + 'ItemSub'); if (n = ro.previousSibling) DOM.removeClass(n, 'mceLast'); DOM.addClass(ro, 'mceLast'); } }); })(tinymce);