mxPopupMenu.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. /**
  2. * Copyright (c) 2006-2015, JGraph Ltd
  3. * Copyright (c) 2006-2015, Gaudenz Alder
  4. */
  5. /**
  6. * Class: mxPopupMenu
  7. *
  8. * Basic popup menu. To add a vertical scrollbar to a given submenu, the
  9. * following code can be used.
  10. *
  11. * (code)
  12. * var mxPopupMenuShowMenu = mxPopupMenu.prototype.showMenu;
  13. * mxPopupMenu.prototype.showMenu = function()
  14. * {
  15. * mxPopupMenuShowMenu.apply(this, arguments);
  16. *
  17. * this.div.style.overflowY = 'auto';
  18. * this.div.style.overflowX = 'hidden';
  19. * this.div.style.maxHeight = '160px';
  20. * };
  21. * (end)
  22. *
  23. * Constructor: mxPopupMenu
  24. *
  25. * Constructs a popupmenu.
  26. *
  27. * Event: mxEvent.SHOW
  28. *
  29. * Fires after the menu has been shown in <popup>.
  30. */
  31. function mxPopupMenu(factoryMethod)
  32. {
  33. this.factoryMethod = factoryMethod;
  34. if (factoryMethod != null)
  35. {
  36. this.init();
  37. }
  38. };
  39. /**
  40. * Extends mxEventSource.
  41. */
  42. mxPopupMenu.prototype = new mxEventSource();
  43. mxPopupMenu.prototype.constructor = mxPopupMenu;
  44. /**
  45. * Variable: submenuImage
  46. *
  47. * URL of the image to be used for the submenu icon.
  48. */
  49. mxPopupMenu.prototype.submenuImage = mxClient.imageBasePath + '/submenu.gif';
  50. /**
  51. * Variable: zIndex
  52. *
  53. * Specifies the zIndex for the popupmenu and its shadow. Default is 10006.
  54. */
  55. mxPopupMenu.prototype.zIndex = 10006;
  56. /**
  57. * Variable: factoryMethod
  58. *
  59. * Function that is used to create the popup menu. The function takes the
  60. * current panning handler, the <mxCell> under the mouse and the mouse
  61. * event that triggered the call as arguments.
  62. */
  63. mxPopupMenu.prototype.factoryMethod = null;
  64. /**
  65. * Variable: useLeftButtonForPopup
  66. *
  67. * Specifies if popupmenus should be activated by clicking the left mouse
  68. * button. Default is false.
  69. */
  70. mxPopupMenu.prototype.useLeftButtonForPopup = false;
  71. /**
  72. * Variable: enabled
  73. *
  74. * Specifies if events are handled. Default is true.
  75. */
  76. mxPopupMenu.prototype.enabled = true;
  77. /**
  78. * Variable: itemCount
  79. *
  80. * Contains the number of times <addItem> has been called for a new menu.
  81. */
  82. mxPopupMenu.prototype.itemCount = 0;
  83. /**
  84. * Variable: autoExpand
  85. *
  86. * Specifies if submenus should be expanded on mouseover. Default is false.
  87. */
  88. mxPopupMenu.prototype.autoExpand = false;
  89. /**
  90. * Variable: smartSeparators
  91. *
  92. * Specifies if separators should only be added if a menu item follows them.
  93. * Default is false.
  94. */
  95. mxPopupMenu.prototype.smartSeparators = false;
  96. /**
  97. * Variable: labels
  98. *
  99. * Specifies if any labels should be visible. Default is true.
  100. */
  101. mxPopupMenu.prototype.labels = true;
  102. /**
  103. * Function: init
  104. *
  105. * Initializes the shapes required for this vertex handler.
  106. */
  107. mxPopupMenu.prototype.init = function()
  108. {
  109. // Adds the inner table
  110. this.table = document.createElement('table');
  111. this.table.className = 'mxPopupMenu';
  112. this.tbody = document.createElement('tbody');
  113. this.table.appendChild(this.tbody);
  114. // Adds the outer div
  115. this.div = document.createElement('div');
  116. this.div.className = 'mxPopupMenu';
  117. this.div.style.display = 'inline';
  118. this.div.style.zIndex = this.zIndex;
  119. this.div.appendChild(this.table);
  120. // Disables the context menu on the outer div
  121. mxEvent.disableContextMenu(this.div);
  122. };
  123. /**
  124. * Function: isEnabled
  125. *
  126. * Returns true if events are handled. This implementation
  127. * returns <enabled>.
  128. */
  129. mxPopupMenu.prototype.isEnabled = function()
  130. {
  131. return this.enabled;
  132. };
  133. /**
  134. * Function: setEnabled
  135. *
  136. * Enables or disables event handling. This implementation
  137. * updates <enabled>.
  138. */
  139. mxPopupMenu.prototype.setEnabled = function(enabled)
  140. {
  141. this.enabled = enabled;
  142. };
  143. /**
  144. * Function: isPopupTrigger
  145. *
  146. * Returns true if the given event is a popupmenu trigger for the optional
  147. * given cell.
  148. *
  149. * Parameters:
  150. *
  151. * me - <mxMouseEvent> that represents the mouse event.
  152. */
  153. mxPopupMenu.prototype.isPopupTrigger = function(me)
  154. {
  155. return me.isPopupTrigger() || (this.useLeftButtonForPopup && mxEvent.isLeftMouseButton(me.getEvent()));
  156. };
  157. /**
  158. * Function: addItem
  159. *
  160. * Adds the given item to the given parent item. If no parent item is specified
  161. * then the item is added to the top-level menu. The return value may be used
  162. * as the parent argument, ie. as a submenu item. The return value is the table
  163. * row that represents the item.
  164. *
  165. * Paramters:
  166. *
  167. * title - String that represents the title of the menu item.
  168. * image - Optional URL for the image icon.
  169. * funct - Function associated that takes a mouseup or touchend event.
  170. * parent - Optional item returned by <addItem>.
  171. * iconCls - Optional string that represents the CSS class for the image icon.
  172. * IconsCls is ignored if image is given.
  173. * enabled - Optional boolean indicating if the item is enabled. Default is true.
  174. * active - Optional boolean indicating if the menu should implement any event handling.
  175. * Default is true.
  176. * noHover - Optional boolean to disable hover state.
  177. */
  178. mxPopupMenu.prototype.addItem = function(title, image, funct, parent, iconCls, enabled, active, noHover)
  179. {
  180. parent = parent || this;
  181. this.itemCount++;
  182. // Smart separators only added if element contains items
  183. if (parent.willAddSeparator)
  184. {
  185. if (parent.containsItems)
  186. {
  187. this.addSeparator(parent, true);
  188. }
  189. parent.willAddSeparator = false;
  190. }
  191. parent.containsItems = true;
  192. var tr = document.createElement('tr');
  193. tr.className = 'mxPopupMenuItem';
  194. var col1 = document.createElement('td');
  195. col1.className = 'mxPopupMenuIcon';
  196. // Adds the given image into the first column
  197. if (image != null)
  198. {
  199. var img = document.createElement('img');
  200. img.src = image;
  201. col1.appendChild(img);
  202. }
  203. else if (iconCls != null)
  204. {
  205. var div = document.createElement('div');
  206. div.className = iconCls;
  207. col1.appendChild(div);
  208. }
  209. tr.appendChild(col1);
  210. if (this.labels)
  211. {
  212. var col2 = document.createElement('td');
  213. col2.className = 'mxPopupMenuItem' +
  214. ((enabled != null && !enabled) ? ' mxDisabled' : '');
  215. mxUtils.write(col2, title);
  216. col2.align = 'left';
  217. tr.appendChild(col2);
  218. var col3 = document.createElement('td');
  219. col3.className = 'mxPopupMenuItem' +
  220. ((enabled != null && !enabled) ? ' mxDisabled' : '');
  221. col3.style.paddingRight = '6px';
  222. col3.style.textAlign = 'right';
  223. tr.appendChild(col3);
  224. if (parent.div == null)
  225. {
  226. this.createSubmenu(parent);
  227. }
  228. }
  229. parent.tbody.appendChild(tr);
  230. if (active != false && enabled != false)
  231. {
  232. var currentSelection = null;
  233. mxEvent.addGestureListeners(tr,
  234. mxUtils.bind(this, function(evt)
  235. {
  236. this.eventReceiver = tr;
  237. if (parent.activeRow != tr && parent.activeRow != parent)
  238. {
  239. if (parent.activeRow != null && parent.activeRow.div.parentNode != null)
  240. {
  241. this.hideSubmenu(parent);
  242. }
  243. if (tr.div != null)
  244. {
  245. this.showSubmenu(parent, tr);
  246. parent.activeRow = tr;
  247. }
  248. }
  249. // Workaround for lost current selection in page because of focus in IE
  250. if (document.selection != null && (mxClient.IS_QUIRKS || document.documentMode == 8))
  251. {
  252. currentSelection = document.selection.createRange();
  253. }
  254. mxEvent.consume(evt);
  255. }),
  256. mxUtils.bind(this, function(evt)
  257. {
  258. if (parent.activeRow != tr && parent.activeRow != parent)
  259. {
  260. if (parent.activeRow != null && parent.activeRow.div.parentNode != null)
  261. {
  262. this.hideSubmenu(parent);
  263. }
  264. if (this.autoExpand && tr.div != null)
  265. {
  266. this.showSubmenu(parent, tr);
  267. parent.activeRow = tr;
  268. }
  269. }
  270. // Sets hover style because TR in IE doesn't have hover
  271. if (!noHover)
  272. {
  273. tr.className = 'mxPopupMenuItemHover';
  274. }
  275. }),
  276. mxUtils.bind(this, function(evt)
  277. {
  278. // EventReceiver avoids clicks on a submenu item
  279. // which has just been shown in the mousedown
  280. if (this.eventReceiver == tr)
  281. {
  282. if (parent.activeRow != tr)
  283. {
  284. this.hideMenu();
  285. }
  286. // Workaround for lost current selection in page because of focus in IE
  287. if (currentSelection != null)
  288. {
  289. // Workaround for "unspecified error" in IE8 standards
  290. try
  291. {
  292. currentSelection.select();
  293. }
  294. catch (e)
  295. {
  296. // ignore
  297. }
  298. currentSelection = null;
  299. }
  300. if (funct != null)
  301. {
  302. funct(evt);
  303. }
  304. }
  305. this.eventReceiver = null;
  306. mxEvent.consume(evt);
  307. })
  308. );
  309. // Resets hover style because TR in IE doesn't have hover
  310. if (!noHover)
  311. {
  312. mxEvent.addListener(tr, 'mouseout',
  313. mxUtils.bind(this, function(evt)
  314. {
  315. tr.className = 'mxPopupMenuItem';
  316. })
  317. );
  318. }
  319. }
  320. return tr;
  321. };
  322. /**
  323. * Adds a checkmark to the given menuitem.
  324. */
  325. mxPopupMenu.prototype.addCheckmark = function(item, img)
  326. {
  327. var td = item.firstChild.nextSibling;
  328. td.style.backgroundImage = 'url(\'' + img + '\')';
  329. td.style.backgroundRepeat = 'no-repeat';
  330. td.style.backgroundPosition = '2px 50%';
  331. };
  332. /**
  333. * Function: createSubmenu
  334. *
  335. * Creates the nodes required to add submenu items inside the given parent
  336. * item. This is called in <addItem> if a parent item is used for the first
  337. * time. This adds various DOM nodes and a <submenuImage> to the parent.
  338. *
  339. * Parameters:
  340. *
  341. * parent - An item returned by <addItem>.
  342. */
  343. mxPopupMenu.prototype.createSubmenu = function(parent)
  344. {
  345. parent.table = document.createElement('table');
  346. parent.table.className = 'mxPopupMenu';
  347. parent.tbody = document.createElement('tbody');
  348. parent.table.appendChild(parent.tbody);
  349. parent.div = document.createElement('div');
  350. parent.div.className = 'mxPopupMenu';
  351. parent.div.style.position = 'absolute';
  352. parent.div.style.display = 'inline';
  353. parent.div.style.zIndex = this.zIndex;
  354. parent.div.appendChild(parent.table);
  355. var img = document.createElement('img');
  356. img.setAttribute('src', this.submenuImage);
  357. // Last column of the submenu item in the parent menu
  358. td = parent.firstChild.nextSibling.nextSibling;
  359. td.appendChild(img);
  360. };
  361. /**
  362. * Function: showSubmenu
  363. *
  364. * Shows the submenu inside the given parent row.
  365. */
  366. mxPopupMenu.prototype.showSubmenu = function(parent, row)
  367. {
  368. if (row.div != null)
  369. {
  370. row.div.style.left = (parent.div.offsetLeft +
  371. row.offsetLeft+row.offsetWidth - 1) + 'px';
  372. row.div.style.top = (parent.div.offsetTop+row.offsetTop) + 'px';
  373. document.body.appendChild(row.div);
  374. // Moves the submenu to the left side if there is no space
  375. var left = parseInt(row.div.offsetLeft);
  376. var width = parseInt(row.div.offsetWidth);
  377. var offset = mxUtils.getDocumentScrollOrigin(document);
  378. var b = document.body;
  379. var d = document.documentElement;
  380. var right = offset.x + (b.clientWidth || d.clientWidth);
  381. if (left + width > right)
  382. {
  383. row.div.style.left = Math.max(0, (parent.div.offsetLeft - width + ((mxClient.IS_IE) ? 6 : -6))) + 'px';
  384. }
  385. mxUtils.fit(row.div);
  386. }
  387. };
  388. /**
  389. * Function: addSeparator
  390. *
  391. * Adds a horizontal separator in the given parent item or the top-level menu
  392. * if no parent is specified.
  393. *
  394. * Parameters:
  395. *
  396. * parent - Optional item returned by <addItem>.
  397. * force - Optional boolean to ignore <smartSeparators>. Default is false.
  398. */
  399. mxPopupMenu.prototype.addSeparator = function(parent, force)
  400. {
  401. parent = parent || this;
  402. if (this.smartSeparators && !force)
  403. {
  404. parent.willAddSeparator = true;
  405. }
  406. else if (parent.tbody != null)
  407. {
  408. parent.willAddSeparator = false;
  409. var tr = document.createElement('tr');
  410. var col1 = document.createElement('td');
  411. col1.className = 'mxPopupMenuIcon';
  412. col1.style.padding = '0 0 0 0px';
  413. tr.appendChild(col1);
  414. var col2 = document.createElement('td');
  415. col2.style.padding = '0 0 0 0px';
  416. col2.setAttribute('colSpan', '2');
  417. var hr = document.createElement('hr');
  418. hr.setAttribute('size', '1');
  419. col2.appendChild(hr);
  420. tr.appendChild(col2);
  421. parent.tbody.appendChild(tr);
  422. }
  423. };
  424. /**
  425. * Function: popup
  426. *
  427. * Shows the popup menu for the given event and cell.
  428. *
  429. * Example:
  430. *
  431. * (code)
  432. * graph.panningHandler.popup = function(x, y, cell, evt)
  433. * {
  434. * mxUtils.alert('Hello, World!');
  435. * }
  436. * (end)
  437. */
  438. mxPopupMenu.prototype.popup = function(x, y, cell, evt)
  439. {
  440. if (this.div != null && this.tbody != null && this.factoryMethod != null)
  441. {
  442. this.div.style.left = x + 'px';
  443. this.div.style.top = y + 'px';
  444. // Removes all child nodes from the existing menu
  445. while (this.tbody.firstChild != null)
  446. {
  447. mxEvent.release(this.tbody.firstChild);
  448. this.tbody.removeChild(this.tbody.firstChild);
  449. }
  450. this.itemCount = 0;
  451. this.factoryMethod(this, cell, evt);
  452. if (this.itemCount > 0)
  453. {
  454. this.showMenu();
  455. this.fireEvent(new mxEventObject(mxEvent.SHOW));
  456. }
  457. }
  458. };
  459. /**
  460. * Function: isMenuShowing
  461. *
  462. * Returns true if the menu is showing.
  463. */
  464. mxPopupMenu.prototype.isMenuShowing = function()
  465. {
  466. return this.div != null && this.div.parentNode == document.body;
  467. };
  468. /**
  469. * Function: showMenu
  470. *
  471. * Shows the menu.
  472. */
  473. mxPopupMenu.prototype.showMenu = function()
  474. {
  475. // Disables filter-based shadow in IE9 standards mode
  476. if (document.documentMode >= 9)
  477. {
  478. this.div.style.filter = 'none';
  479. }
  480. // Fits the div inside the viewport
  481. document.body.appendChild(this.div);
  482. mxUtils.fit(this.div);
  483. };
  484. /**
  485. * Function: hideMenu
  486. *
  487. * Removes the menu and all submenus.
  488. */
  489. mxPopupMenu.prototype.hideMenu = function()
  490. {
  491. if (this.div != null)
  492. {
  493. if (this.div.parentNode != null)
  494. {
  495. this.div.parentNode.removeChild(this.div);
  496. }
  497. this.hideSubmenu(this);
  498. this.containsItems = false;
  499. this.fireEvent(new mxEventObject(mxEvent.HIDE));
  500. }
  501. };
  502. /**
  503. * Function: hideSubmenu
  504. *
  505. * Removes all submenus inside the given parent.
  506. *
  507. * Parameters:
  508. *
  509. * parent - An item returned by <addItem>.
  510. */
  511. mxPopupMenu.prototype.hideSubmenu = function(parent)
  512. {
  513. if (parent.activeRow != null)
  514. {
  515. this.hideSubmenu(parent.activeRow);
  516. if (parent.activeRow.div.parentNode != null)
  517. {
  518. parent.activeRow.div.parentNode.removeChild(parent.activeRow.div);
  519. }
  520. parent.activeRow = null;
  521. }
  522. };
  523. /**
  524. * Function: destroy
  525. *
  526. * Destroys the handler and all its resources and DOM nodes.
  527. */
  528. mxPopupMenu.prototype.destroy = function()
  529. {
  530. if (this.div != null)
  531. {
  532. mxEvent.release(this.div);
  533. if (this.div.parentNode != null)
  534. {
  535. this.div.parentNode.removeChild(this.div);
  536. }
  537. this.div = null;
  538. }
  539. };