4 * Copyright 2009, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://tinymce.moxiecode.com/license
8 * Contributing: http://tinymce.moxiecode.com/contributing
12 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element;
15 * This class is used to create drop menus, a drop menu can be a
16 * context menu, or a menu for a list box or a menu bar.
18 * @class tinymce.ui.DropMenu
19 * @extends tinymce.ui.Menu
21 tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', {
23 * Constructs a new drop menu control instance.
27 * @param {String} id Button control id for the button.
28 * @param {Object} s Optional name/value settings object.
30 DropMenu : function(id, s) {
32 s.container = s.container || DOM.doc.body;
33 s.offset_x = s.offset_x || 0;
34 s.offset_y = s.offset_y || 0;
35 s.vp_offset_x = s.vp_offset_x || 0;
36 s.vp_offset_y = s.vp_offset_y || 0;
38 if (is(s.icons) && !s.icons)
39 s['class'] += ' mceNoIcons';
42 this.onShowMenu = new tinymce.util.Dispatcher(this);
43 this.onHideMenu = new tinymce.util.Dispatcher(this);
44 this.classPrefix = 'mceMenu';
48 * Created a new sub menu for the drop menu control.
51 * @param {Object} s Optional name/value settings object.
52 * @return {tinymce.ui.DropMenu} New drop menu instance.
54 createMenu : function(s) {
55 var t = this, cs = t.settings, m;
57 s.container = s.container || cs.container;
59 s.constrain = s.constrain || cs.constrain;
60 s['class'] = s['class'] || cs['class'];
61 s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x;
62 s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y;
63 s.keyboard_focus = cs.keyboard_focus;
64 m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s);
66 m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem);
74 t.keyboardNav.focus();
79 * Repaints the menu after new items have been added dynamically.
84 var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th;
86 tw = s.max_width ? Math.min(tb.clientWidth, s.max_width) : tb.clientWidth;
87 th = s.max_height ? Math.min(tb.clientHeight, s.max_height) : tb.clientHeight;
90 t.element.setStyles({width : tw + 2, height : th + 2});
92 t.element.setStyles({width : tw, height : th});
95 DOM.setStyle(co, 'width', tw);
98 DOM.setStyle(co, 'height', th);
100 if (tb.clientHeight < s.max_height)
101 DOM.setStyle(co, 'overflow', 'hidden');
106 * Displays the menu at the specified cordinate.
109 * @param {Number} x Horizontal position of the menu.
110 * @param {Number} y Vertical position of the menu.
111 * @param {Numner} px Optional parent X position used when menus are cascading.
113 showMenu : function(x, y, px) {
114 var t = this, s = t.settings, co, vp = DOM.getViewPort(), w, h, mx, my, ot = 2, dm, tb, cp = t.classPrefix;
122 co = DOM.add(t.settings.container, t.renderNode());
124 each(t.items, function(o) {
128 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container});
130 co = DOM.get('menu_' + t.id);
132 // Move layer out of sight unless it's Opera since it scrolls to top of page due to an bug
133 if (!tinymce.isOpera)
134 DOM.setStyles(co, {left : -0xFFFF , top : -0xFFFF});
139 x += s.offset_x || 0;
140 y += s.offset_y || 0;
144 // Move inside viewport if not submenu
146 w = co.clientWidth - ot;
147 h = co.clientHeight - ot;
151 if ((x + s.vp_offset_x + w) > mx)
152 x = px ? px - w : Math.max(0, (mx - s.vp_offset_x) - w);
154 if ((y + s.vp_offset_y + h) > my)
155 y = Math.max(0, (my - s.vp_offset_y) - h);
158 DOM.setStyles(co, {left : x , top : y});
162 t.mouseClickFunc = Event.add(co, 'click', function(e) {
167 if (e && (e = DOM.getParent(e, 'tr')) && !DOM.hasClass(e, cp + 'ItemSub')) {
179 dm = dm.settings.parent;
182 if (m.settings.onclick)
183 m.settings.onclick(e);
185 return Event.cancel(e); // Cancel to fix onbeforeunload problem
190 t.mouseOverFunc = Event.add(co, 'mouseover', function(e) {
194 if (e && (e = DOM.getParent(e, 'tr'))) {
198 t.lastMenu.collapse(1);
203 if (e && DOM.hasClass(e, cp + 'ItemSub')) {
204 //p = DOM.getPos(s.container);
206 m.showMenu((r.x + r.w - ot), r.y - ot, r.x);
208 DOM.addClass(DOM.get(m.id).firstChild, cp + 'ItemActive');
214 Event.add(co, 'keydown', t._keyHandler, t);
216 t.onShowMenu.dispatch(t);
218 if (s.keyboard_focus) {
219 t._setupKeyboardNav();
224 * Hides the displayed menu.
228 hideMenu : function(c) {
229 var t = this, co = DOM.get('menu_' + t.id), e;
231 if (!t.isMenuVisible)
234 if (t.keyboardNav) t.keyboardNav.destroy();
235 Event.remove(co, 'mouseover', t.mouseOverFunc);
236 Event.remove(co, 'click', t.mouseClickFunc);
237 Event.remove(co, 'keydown', t._keyHandler);
247 if (e = DOM.get(t.id))
248 DOM.removeClass(e.firstChild, t.classPrefix + 'ItemActive');
250 t.onHideMenu.dispatch(t);
254 * Adds a new menu, menu item or sub classes of them to the drop menu.
257 * @param {tinymce.ui.Control} o Menu or menu item to add to the drop menu.
258 * @return {tinymce.ui.Control} Same as the input control, the menu or menu item.
265 if (t.isRendered && (co = DOM.get('menu_' + t.id)))
266 t._add(DOM.select('tbody', co)[0], o);
272 * Collapses the menu, this will hide the menu and all menu items.
275 * @param {Boolean} d Optional deep state. If this is set to true all children will be collapsed as well.
277 collapse : function(d) {
283 * Removes a specific sub menu or menu item from the drop menu.
286 * @param {tinymce.ui.Control} o Menu item or menu to remove from drop menu.
287 * @return {tinymce.ui.Control} Control instance or null if it wasn't found.
289 remove : function(o) {
293 return this.parent(o);
297 * Destroys the menu. This will remove the menu from the DOM and any events added to it etc.
301 destroy : function() {
302 var t = this, co = DOM.get('menu_' + t.id);
304 if (t.keyboardNav) t.keyboardNav.destroy();
305 Event.remove(co, 'mouseover', t.mouseOverFunc);
306 Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc);
307 Event.remove(co, 'click', t.mouseClickFunc);
308 Event.remove(co, 'keydown', t._keyHandler);
317 * Renders the specified menu node to the dom.
320 * @return {Element} Container element for the drop menu.
322 renderNode : function() {
323 var t = this, s = t.settings, n, tb, co, w;
325 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'});
326 if (t.settings.parent) {
327 DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id);
329 co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')});
330 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container});
333 DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'});
335 // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'});
336 n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0});
337 tb = DOM.add(n, 'tbody');
339 each(t.items, function(o) {
348 // Internal functions
349 _setupKeyboardNav : function(){
350 var contextMenu, menuItems, t=this;
351 contextMenu = DOM.select('#menu_' + t.id)[0];
352 menuItems = DOM.select('a[role=option]', 'menu_' + t.id);
353 menuItems.splice(0,0,contextMenu);
354 t.keyboardNav = new tinymce.ui.KeyboardNavigation({
355 root: 'menu_' + t.id,
357 onCancel: function() {
365 _keyHandler : function(evt) {
367 switch (evt.keyCode) {
369 if (t.settings.parent) {
371 t.settings.parent.focus();
377 t.mouseOverFunc(evt);
382 _add : function(tb, o) {
383 var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic;
386 ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'});
387 DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'});
389 if (n = ro.previousSibling)
390 DOM.addClass(n, 'mceLast');
395 n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'});
396 n = it = DOM.add(n, s.titleItem ? 'th' : 'td');
397 n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'});
400 DOM.setAttrib(a, 'aria-haspopup', 'true');
401 DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id);
404 DOM.addClass(it, s['class']);
405 // n = DOM.add(n, 'span', {'class' : 'item'});
407 ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')});
410 DOM.add(ic, 'img', {src : s.icon_src});
412 n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title);
414 if (o.settings.style)
415 DOM.setAttrib(n, 'style', o.settings.style);
417 if (tb.childNodes.length == 1)
418 DOM.addClass(ro, 'mceFirst');
420 if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator'))
421 DOM.addClass(ro, 'mceFirst');
424 DOM.addClass(ro, cp + 'ItemSub');
426 if (n = ro.previousSibling)
427 DOM.removeClass(n, 'mceLast');
429 DOM.addClass(ro, 'mceLast');