jquery.contextMenu.js 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686
  1. /*!
  2. * jQuery contextMenu - Plugin for simple contextMenu handling
  3. *
  4. * Version: 1.6.5
  5. *
  6. * Authors: Rodney Rehm, Addy Osmani (patches for FF)
  7. * Web: http://medialize.github.com/jQuery-contextMenu/
  8. *
  9. * Licensed under
  10. * MIT License http://www.opensource.org/licenses/mit-license
  11. * GPL v3 http://opensource.org/licenses/GPL-3.0
  12. *
  13. */
  14. (function($, undefined){
  15. // TODO: -
  16. // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
  17. // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative
  18. // determine html5 compatibility
  19. $.support.htmlMenuitem = ('HTMLMenuItemElement' in window);
  20. $.support.htmlCommand = ('HTMLCommandElement' in window);
  21. $.support.eventSelectstart = ("onselectstart" in document.documentElement);
  22. /* // should the need arise, test for css user-select
  23. $.support.cssUserSelect = (function(){
  24. var t = false,
  25. e = document.createElement('div');
  26. $.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) {
  27. var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect',
  28. prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select';
  29. e.style.cssText = prop + ': text;';
  30. if (e.style[propCC] == 'text') {
  31. t = true;
  32. return false;
  33. }
  34. return true;
  35. });
  36. return t;
  37. })();
  38. */
  39. if (!$.ui || !$.ui.widget) {
  40. // duck punch $.cleanData like jQueryUI does to get that remove event
  41. // https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js#L16-24
  42. var _cleanData = $.cleanData;
  43. $.cleanData = function( elems ) {
  44. for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
  45. try {
  46. $( elem ).triggerHandler( "remove" );
  47. // http://bugs.jquery.com/ticket/8235
  48. } catch( e ) {}
  49. }
  50. _cleanData( elems );
  51. };
  52. }
  53. var // currently active contextMenu trigger
  54. $currentTrigger = null,
  55. // is contextMenu initialized with at least one menu?
  56. initialized = false,
  57. // window handle
  58. $win = $(window),
  59. // number of registered menus
  60. counter = 0,
  61. // mapping selector to namespace
  62. namespaces = {},
  63. // mapping namespace to options
  64. menus = {},
  65. // custom command type handlers
  66. types = {},
  67. // default values
  68. defaults = {
  69. // selector of contextMenu trigger
  70. selector: null,
  71. // where to append the menu to
  72. appendTo: null,
  73. // method to trigger context menu ["right", "left", "hover"]
  74. trigger: "right",
  75. // hide menu when mouse leaves trigger / menu elements
  76. autoHide: false,
  77. // ms to wait before showing a hover-triggered context menu
  78. delay: 200,
  79. // flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu
  80. // as long as the trigger happened on one of the trigger-element's child nodes
  81. reposition: true,
  82. // determine position to show menu at
  83. determinePosition: function($menu) {
  84. // position to the lower middle of the trigger element
  85. if ($.ui && $.ui.position) {
  86. // .position() is provided as a jQuery UI utility
  87. // (...and it won't work on hidden elements)
  88. $menu.css('display', 'block').position({
  89. my: "center top",
  90. at: "center bottom",
  91. of: this,
  92. offset: "0 5",
  93. collision: "fit"
  94. }).css('display', 'none');
  95. } else {
  96. // determine contextMenu position
  97. var offset = this.offset();
  98. offset.top += this.outerHeight();
  99. offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;
  100. $menu.css(offset);
  101. }
  102. },
  103. // position menu
  104. position: function(opt, x, y) {
  105. var $this = this,
  106. offset;
  107. // determine contextMenu position
  108. if (!x && !y) {
  109. opt.determinePosition.call(this, opt.$menu);
  110. return;
  111. } else if (x === "maintain" && y === "maintain") {
  112. // x and y must not be changed (after re-show on command click)
  113. offset = opt.$menu.position();
  114. } else {
  115. // x and y are given (by mouse event)
  116. offset = {top: y, left: x};
  117. }
  118. // correct offset if viewport demands it
  119. var bottom = $win.scrollTop() + $win.height(),
  120. right = $win.scrollLeft() + $win.width(),
  121. height = opt.$menu.height(),
  122. width = opt.$menu.width();
  123. if (offset.top + height > bottom) {
  124. offset.top -= height;
  125. }
  126. if (offset.left + width > right) {
  127. offset.left -= width;
  128. }
  129. opt.$menu.css(offset);
  130. },
  131. // position the sub-menu
  132. positionSubmenu: function($menu) {
  133. if ($.ui && $.ui.position) {
  134. // .position() is provided as a jQuery UI utility
  135. // (...and it won't work on hidden elements)
  136. $menu.css('display', 'block').position({
  137. my: "left top",
  138. at: "right top",
  139. of: this,
  140. collision: "flipfit fit"
  141. }).css('display', '');
  142. } else {
  143. // determine contextMenu position
  144. var offset = {
  145. top: 0,
  146. left: this.outerWidth()
  147. };
  148. $menu.css(offset);
  149. }
  150. },
  151. // offset to add to zIndex
  152. zIndex: 1,
  153. // show hide animation settings
  154. animation: {
  155. duration: 50,
  156. show: 'slideDown',
  157. hide: 'slideUp'
  158. },
  159. // events
  160. events: {
  161. show: $.noop,
  162. hide: $.noop
  163. },
  164. // default callback
  165. callback: null,
  166. // list of contextMenu items
  167. items: {}
  168. },
  169. // mouse position for hover activation
  170. hoveract = {
  171. timer: null,
  172. pageX: null,
  173. pageY: null
  174. },
  175. // determine zIndex
  176. zindex = function($t) {
  177. var zin = 0,
  178. $tt = $t;
  179. while (true) {
  180. zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);
  181. $tt = $tt.parent();
  182. if (!$tt || !$tt.length || "html body".indexOf($tt.prop('nodeName').toLowerCase()) > -1 ) {
  183. break;
  184. }
  185. }
  186. return zin;
  187. },
  188. // event handlers
  189. handle = {
  190. // abort anything
  191. abortevent: function(e){
  192. e.preventDefault();
  193. e.stopImmediatePropagation();
  194. },
  195. // contextmenu show dispatcher
  196. contextmenu: function(e) {
  197. var $this = $(this);
  198. // disable actual context-menu
  199. e.preventDefault();
  200. e.stopImmediatePropagation();
  201. // abort native-triggered events unless we're triggering on right click
  202. if (e.data.trigger != 'right' && e.originalEvent) {
  203. return;
  204. }
  205. // abort event if menu is visible for this trigger
  206. if ($this.hasClass('context-menu-active')) {
  207. return;
  208. }
  209. if (!$this.hasClass('context-menu-disabled')) {
  210. // theoretically need to fire a show event at <menu>
  211. // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
  212. // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });
  213. // e.data.$menu.trigger(evt);
  214. $currentTrigger = $this;
  215. if (e.data.build) {
  216. var built = e.data.build($currentTrigger, e);
  217. // abort if build() returned false
  218. if (built === false) {
  219. return;
  220. }
  221. // dynamically build menu on invocation
  222. e.data = $.extend(true, {}, defaults, e.data, built || {});
  223. // abort if there are no items to display
  224. if (!e.data.items || $.isEmptyObject(e.data.items)) {
  225. // Note: jQuery captures and ignores errors from event handlers
  226. if (window.console) {
  227. (console.error || console.log)("No items specified to show in contextMenu");
  228. }
  229. throw new Error('No Items sepcified');
  230. }
  231. // backreference for custom command type creation
  232. e.data.$trigger = $currentTrigger;
  233. op.create(e.data);
  234. }
  235. // show menu
  236. op.show.call($this, e.data, e.pageX, e.pageY);
  237. }
  238. },
  239. // contextMenu left-click trigger
  240. click: function(e) {
  241. e.preventDefault();
  242. e.stopImmediatePropagation();
  243. $(this).trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
  244. },
  245. // contextMenu right-click trigger
  246. mousedown: function(e) {
  247. // register mouse down
  248. var $this = $(this);
  249. // hide any previous menus
  250. if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {
  251. $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');
  252. }
  253. // activate on right click
  254. if (e.button == 2) {
  255. $currentTrigger = $this.data('contextMenuActive', true);
  256. }
  257. },
  258. // contextMenu right-click trigger
  259. mouseup: function(e) {
  260. // show menu
  261. var $this = $(this);
  262. if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {
  263. e.preventDefault();
  264. e.stopImmediatePropagation();
  265. $currentTrigger = $this;
  266. $this.trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
  267. }
  268. $this.removeData('contextMenuActive');
  269. },
  270. // contextMenu hover trigger
  271. mouseenter: function(e) {
  272. var $this = $(this),
  273. $related = $(e.relatedTarget),
  274. $document = $(document);
  275. // abort if we're coming from a menu
  276. if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
  277. return;
  278. }
  279. // abort if a menu is shown
  280. if ($currentTrigger && $currentTrigger.length) {
  281. return;
  282. }
  283. hoveract.pageX = e.pageX;
  284. hoveract.pageY = e.pageY;
  285. hoveract.data = e.data;
  286. $document.on('mousemove.contextMenuShow', handle.mousemove);
  287. hoveract.timer = setTimeout(function() {
  288. hoveract.timer = null;
  289. $document.off('mousemove.contextMenuShow');
  290. $currentTrigger = $this;
  291. $this.trigger($.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY }));
  292. }, e.data.delay );
  293. },
  294. // contextMenu hover trigger
  295. mousemove: function(e) {
  296. hoveract.pageX = e.pageX;
  297. hoveract.pageY = e.pageY;
  298. },
  299. // contextMenu hover trigger
  300. mouseleave: function(e) {
  301. // abort if we're leaving for a menu
  302. var $related = $(e.relatedTarget);
  303. if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
  304. return;
  305. }
  306. try {
  307. clearTimeout(hoveract.timer);
  308. } catch(e) {}
  309. hoveract.timer = null;
  310. },
  311. // click on layer to hide contextMenu
  312. layerClick: function(e) {
  313. var $this = $(this),
  314. root = $this.data('contextMenuRoot'),
  315. mouseup = false,
  316. button = e.button,
  317. x = e.pageX,
  318. y = e.pageY,
  319. target,
  320. offset,
  321. selectors;
  322. e.preventDefault();
  323. e.stopImmediatePropagation();
  324. setTimeout(function() {
  325. var $window, hideshow, possibleTarget;
  326. var triggerAction = ((root.trigger == 'left' && button === 0) || (root.trigger == 'right' && button === 2));
  327. // find the element that would've been clicked, wasn't the layer in the way
  328. if (document.elementFromPoint) {
  329. root.$layer.hide();
  330. target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop());
  331. root.$layer.show();
  332. }
  333. if (root.reposition && triggerAction) {
  334. if (document.elementFromPoint) {
  335. if (root.$trigger.is(target) || root.$trigger.has(target).length) {
  336. root.position.call(root.$trigger, root, x, y);
  337. return;
  338. }
  339. } else {
  340. offset = root.$trigger.offset();
  341. $window = $(window);
  342. // while this looks kinda awful, it's the best way to avoid
  343. // unnecessarily calculating any positions
  344. offset.top += $window.scrollTop();
  345. if (offset.top <= e.pageY) {
  346. offset.left += $window.scrollLeft();
  347. if (offset.left <= e.pageX) {
  348. offset.bottom = offset.top + root.$trigger.outerHeight();
  349. if (offset.bottom >= e.pageY) {
  350. offset.right = offset.left + root.$trigger.outerWidth();
  351. if (offset.right >= e.pageX) {
  352. // reposition
  353. root.position.call(root.$trigger, root, x, y);
  354. return;
  355. }
  356. }
  357. }
  358. }
  359. }
  360. }
  361. if (target && triggerAction) {
  362. root.$trigger.one('contextmenu:hidden', function() {
  363. $(target).contextMenu({x: x, y: y});
  364. });
  365. }
  366. root.$menu.trigger('contextmenu:hide');
  367. }, 50);
  368. },
  369. // key handled :hover
  370. keyStop: function(e, opt) {
  371. if (!opt.isInput) {
  372. e.preventDefault();
  373. }
  374. e.stopPropagation();
  375. },
  376. key: function(e) {
  377. var opt = $currentTrigger.data('contextMenu') || {};
  378. switch (e.keyCode) {
  379. case 9:
  380. case 38: // up
  381. handle.keyStop(e, opt);
  382. // if keyCode is [38 (up)] or [9 (tab) with shift]
  383. if (opt.isInput) {
  384. if (e.keyCode == 9 && e.shiftKey) {
  385. e.preventDefault();
  386. opt.$selected && opt.$selected.find('input, textarea, select').blur();
  387. opt.$menu.trigger('prevcommand');
  388. return;
  389. } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
  390. // checkboxes don't capture this key
  391. e.preventDefault();
  392. return;
  393. }
  394. } else if (e.keyCode != 9 || e.shiftKey) {
  395. opt.$menu.trigger('prevcommand');
  396. return;
  397. }
  398. // omitting break;
  399. // case 9: // tab - reached through omitted break;
  400. case 40: // down
  401. handle.keyStop(e, opt);
  402. if (opt.isInput) {
  403. if (e.keyCode == 9) {
  404. e.preventDefault();
  405. opt.$selected && opt.$selected.find('input, textarea, select').blur();
  406. opt.$menu.trigger('nextcommand');
  407. return;
  408. } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
  409. // checkboxes don't capture this key
  410. e.preventDefault();
  411. return;
  412. }
  413. } else {
  414. opt.$menu.trigger('nextcommand');
  415. return;
  416. }
  417. break;
  418. case 37: // left
  419. handle.keyStop(e, opt);
  420. if (opt.isInput || !opt.$selected || !opt.$selected.length) {
  421. break;
  422. }
  423. if (!opt.$selected.parent().hasClass('context-menu-root')) {
  424. var $parent = opt.$selected.parent().parent();
  425. opt.$selected.trigger('contextmenu:blur');
  426. opt.$selected = $parent;
  427. return;
  428. }
  429. break;
  430. case 39: // right
  431. handle.keyStop(e, opt);
  432. if (opt.isInput || !opt.$selected || !opt.$selected.length) {
  433. break;
  434. }
  435. var itemdata = opt.$selected.data('contextMenu') || {};
  436. if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {
  437. opt.$selected = null;
  438. itemdata.$selected = null;
  439. itemdata.$menu.trigger('nextcommand');
  440. return;
  441. }
  442. break;
  443. case 35: // end
  444. case 36: // home
  445. if (opt.$selected && opt.$selected.find('input, textarea, select').length) {
  446. return;
  447. } else {
  448. (opt.$selected && opt.$selected.parent() || opt.$menu)
  449. .children(':not(.disabled, .not-selectable)')[e.keyCode == 36 ? 'first' : 'last']()
  450. .trigger('contextmenu:focus');
  451. e.preventDefault();
  452. return;
  453. }
  454. break;
  455. case 13: // enter
  456. handle.keyStop(e, opt);
  457. if (opt.isInput) {
  458. if (opt.$selected && !opt.$selected.is('textarea, select')) {
  459. e.preventDefault();
  460. return;
  461. }
  462. break;
  463. }
  464. opt.$selected && opt.$selected.trigger('mouseup');
  465. return;
  466. case 32: // space
  467. case 33: // page up
  468. case 34: // page down
  469. // prevent browser from scrolling down while menu is visible
  470. handle.keyStop(e, opt);
  471. return;
  472. case 27: // esc
  473. handle.keyStop(e, opt);
  474. opt.$menu.trigger('contextmenu:hide');
  475. return;
  476. default: // 0-9, a-z
  477. var k = (String.fromCharCode(e.keyCode)).toUpperCase();
  478. if (opt.accesskeys[k]) {
  479. // according to the specs accesskeys must be invoked immediately
  480. opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu
  481. ? 'contextmenu:focus'
  482. : 'mouseup'
  483. );
  484. return;
  485. }
  486. break;
  487. }
  488. // pass event to selected item,
  489. // stop propagation to avoid endless recursion
  490. e.stopPropagation();
  491. opt.$selected && opt.$selected.trigger(e);
  492. },
  493. // select previous possible command in menu
  494. prevItem: function(e) {
  495. e.stopPropagation();
  496. var opt = $(this).data('contextMenu') || {};
  497. // obtain currently selected menu
  498. if (opt.$selected) {
  499. var $s = opt.$selected;
  500. opt = opt.$selected.parent().data('contextMenu') || {};
  501. opt.$selected = $s;
  502. }
  503. var $children = opt.$menu.children(),
  504. $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
  505. $round = $prev;
  506. // skip disabled
  507. while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {
  508. if ($prev.prev().length) {
  509. $prev = $prev.prev();
  510. } else {
  511. $prev = $children.last();
  512. }
  513. if ($prev.is($round)) {
  514. // break endless loop
  515. return;
  516. }
  517. }
  518. // leave current
  519. if (opt.$selected) {
  520. handle.itemMouseleave.call(opt.$selected.get(0), e);
  521. }
  522. // activate next
  523. handle.itemMouseenter.call($prev.get(0), e);
  524. // focus input
  525. var $input = $prev.find('input, textarea, select');
  526. if ($input.length) {
  527. $input.focus();
  528. }
  529. },
  530. // select next possible command in menu
  531. nextItem: function(e) {
  532. e.stopPropagation();
  533. var opt = $(this).data('contextMenu') || {};
  534. // obtain currently selected menu
  535. if (opt.$selected) {
  536. var $s = opt.$selected;
  537. opt = opt.$selected.parent().data('contextMenu') || {};
  538. opt.$selected = $s;
  539. }
  540. var $children = opt.$menu.children(),
  541. $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
  542. $round = $next;
  543. // skip disabled
  544. while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {
  545. if ($next.next().length) {
  546. $next = $next.next();
  547. } else {
  548. $next = $children.first();
  549. }
  550. if ($next.is($round)) {
  551. // break endless loop
  552. return;
  553. }
  554. }
  555. // leave current
  556. if (opt.$selected) {
  557. handle.itemMouseleave.call(opt.$selected.get(0), e);
  558. }
  559. // activate next
  560. handle.itemMouseenter.call($next.get(0), e);
  561. // focus input
  562. var $input = $next.find('input, textarea, select');
  563. if ($input.length) {
  564. $input.focus();
  565. }
  566. },
  567. // flag that we're inside an input so the key handler can act accordingly
  568. focusInput: function(e) {
  569. var $this = $(this).closest('.context-menu-item'),
  570. data = $this.data(),
  571. opt = data.contextMenu,
  572. root = data.contextMenuRoot;
  573. root.$selected = opt.$selected = $this;
  574. root.isInput = opt.isInput = true;
  575. },
  576. // flag that we're inside an input so the key handler can act accordingly
  577. blurInput: function(e) {
  578. var $this = $(this).closest('.context-menu-item'),
  579. data = $this.data(),
  580. opt = data.contextMenu,
  581. root = data.contextMenuRoot;
  582. root.isInput = opt.isInput = false;
  583. },
  584. // :hover on menu
  585. menuMouseenter: function(e) {
  586. var root = $(this).data().contextMenuRoot;
  587. root.hovering = true;
  588. },
  589. // :hover on menu
  590. menuMouseleave: function(e) {
  591. var root = $(this).data().contextMenuRoot;
  592. if (root.$layer && root.$layer.is(e.relatedTarget)) {
  593. root.hovering = false;
  594. }
  595. },
  596. // :hover done manually so key handling is possible
  597. itemMouseenter: function(e) {
  598. var $this = $(this),
  599. data = $this.data(),
  600. opt = data.contextMenu,
  601. root = data.contextMenuRoot;
  602. root.hovering = true;
  603. // abort if we're re-entering
  604. if (e && root.$layer && root.$layer.is(e.relatedTarget)) {
  605. e.preventDefault();
  606. e.stopImmediatePropagation();
  607. }
  608. // make sure only one item is selected
  609. (opt.$menu ? opt : root).$menu
  610. .children('.hover').trigger('contextmenu:blur');
  611. if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {
  612. opt.$selected = null;
  613. return;
  614. }
  615. $this.trigger('contextmenu:focus');
  616. },
  617. // :hover done manually so key handling is possible
  618. itemMouseleave: function(e) {
  619. var $this = $(this),
  620. data = $this.data(),
  621. opt = data.contextMenu,
  622. root = data.contextMenuRoot;
  623. if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {
  624. root.$selected && root.$selected.trigger('contextmenu:blur');
  625. e.preventDefault();
  626. e.stopImmediatePropagation();
  627. root.$selected = opt.$selected = opt.$node;
  628. return;
  629. }
  630. $this.trigger('contextmenu:blur');
  631. },
  632. // contextMenu item click
  633. itemClick: function(e) {
  634. var $this = $(this),
  635. data = $this.data(),
  636. opt = data.contextMenu,
  637. root = data.contextMenuRoot,
  638. key = data.contextMenuKey,
  639. callback;
  640. // abort if the key is unknown or disabled or is a menu
  641. if (!opt.items[key] || $this.is('.disabled, .context-menu-submenu, .context-menu-separator, .not-selectable')) {
  642. return;
  643. }
  644. e.preventDefault();
  645. e.stopImmediatePropagation();
  646. if ($.isFunction(root.callbacks[key]) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) {
  647. // item-specific callback
  648. callback = root.callbacks[key];
  649. } else if ($.isFunction(root.callback)) {
  650. // default callback
  651. callback = root.callback;
  652. } else {
  653. // no callback, no action
  654. return;
  655. }
  656. // hide menu if callback doesn't stop that
  657. if (callback.call(root.$trigger, key, root) !== false) {
  658. root.$menu.trigger('contextmenu:hide');
  659. } else if (root.$menu.parent().length) {
  660. op.update.call(root.$trigger, root);
  661. }
  662. },
  663. // ignore click events on input elements
  664. inputClick: function(e) {
  665. e.stopImmediatePropagation();
  666. },
  667. // hide <menu>
  668. hideMenu: function(e, data) {
  669. var root = $(this).data('contextMenuRoot');
  670. op.hide.call(root.$trigger, root, data && data.force);
  671. },
  672. // focus <command>
  673. focusItem: function(e) {
  674. e.stopPropagation();
  675. var $this = $(this),
  676. data = $this.data(),
  677. opt = data.contextMenu,
  678. root = data.contextMenuRoot;
  679. $this.addClass('hover')
  680. .siblings('.hover').trigger('contextmenu:blur');
  681. // remember selected
  682. opt.$selected = root.$selected = $this;
  683. // position sub-menu - do after show so dumb $.ui.position can keep up
  684. if (opt.$node) {
  685. root.positionSubmenu.call(opt.$node, opt.$menu);
  686. }
  687. },
  688. // blur <command>
  689. blurItem: function(e) {
  690. e.stopPropagation();
  691. var $this = $(this),
  692. data = $this.data(),
  693. opt = data.contextMenu,
  694. root = data.contextMenuRoot;
  695. $this.removeClass('hover');
  696. opt.$selected = null;
  697. }
  698. },
  699. // operations
  700. op = {
  701. show: function(opt, x, y) {
  702. var $trigger = $(this),
  703. offset,
  704. css = {};
  705. // hide any open menus
  706. $('#context-menu-layer').trigger('mousedown');
  707. // backreference for callbacks
  708. opt.$trigger = $trigger;
  709. // show event
  710. if (opt.events.show.call($trigger, opt) === false) {
  711. $currentTrigger = null;
  712. return;
  713. }
  714. // create or update context menu
  715. op.update.call($trigger, opt);
  716. // position menu
  717. opt.position.call($trigger, opt, x, y);
  718. // make sure we're in front
  719. if (opt.zIndex) {
  720. css.zIndex = zindex($trigger) + opt.zIndex;
  721. }
  722. // add layer
  723. op.layer.call(opt.$menu, opt, css.zIndex);
  724. // adjust sub-menu zIndexes
  725. opt.$menu.find('ul').css('zIndex', css.zIndex + 1);
  726. // position and show context menu
  727. opt.$menu.css( css )[opt.animation.show](opt.animation.duration, function() {
  728. $trigger.trigger('contextmenu:visible');
  729. });
  730. // make options available and set state
  731. $trigger
  732. .data('contextMenu', opt)
  733. .addClass("context-menu-active");
  734. // register key handler
  735. $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);
  736. // register autoHide handler
  737. if (opt.autoHide) {
  738. // mouse position handler
  739. $(document).on('mousemove.contextMenuAutoHide', function(e) {
  740. // need to capture the offset on mousemove,
  741. // since the page might've been scrolled since activation
  742. var pos = $trigger.offset();
  743. pos.right = pos.left + $trigger.outerWidth();
  744. pos.bottom = pos.top + $trigger.outerHeight();
  745. if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {
  746. // if mouse in menu...
  747. opt.$menu.trigger('contextmenu:hide');
  748. }
  749. });
  750. }
  751. },
  752. hide: function(opt, force) {
  753. var $trigger = $(this);
  754. if (!opt) {
  755. opt = $trigger.data('contextMenu') || {};
  756. }
  757. // hide event
  758. if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) {
  759. return;
  760. }
  761. // remove options and revert state
  762. $trigger
  763. .removeData('contextMenu')
  764. .removeClass("context-menu-active");
  765. if (opt.$layer) {
  766. // keep layer for a bit so the contextmenu event can be aborted properly by opera
  767. setTimeout((function($layer) {
  768. return function(){
  769. $layer.remove();
  770. };
  771. })(opt.$layer), 10);
  772. try {
  773. delete opt.$layer;
  774. } catch(e) {
  775. opt.$layer = null;
  776. }
  777. }
  778. // remove handle
  779. $currentTrigger = null;
  780. // remove selected
  781. opt.$menu.find('.hover').trigger('contextmenu:blur');
  782. opt.$selected = null;
  783. // unregister key and mouse handlers
  784. //$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705
  785. $(document).off('.contextMenuAutoHide').off('keydown.contextMenu');
  786. // hide menu
  787. opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration, function (){
  788. // tear down dynamically built menu after animation is completed.
  789. if (opt.build) {
  790. opt.$menu.remove();
  791. $.each(opt, function(key, value) {
  792. switch (key) {
  793. case 'ns':
  794. case 'selector':
  795. case 'build':
  796. case 'trigger':
  797. return true;
  798. default:
  799. opt[key] = undefined;
  800. try {
  801. delete opt[key];
  802. } catch (e) {}
  803. return true;
  804. }
  805. });
  806. }
  807. setTimeout(function() {
  808. $trigger.trigger('contextmenu:hidden');
  809. }, 10);
  810. });
  811. },
  812. create: function(opt, root) {
  813. if (root === undefined) {
  814. root = opt;
  815. }
  816. // create contextMenu
  817. opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || "").data({
  818. 'contextMenu': opt,
  819. 'contextMenuRoot': root
  820. });
  821. $.each(['callbacks', 'commands', 'inputs'], function(i,k){
  822. opt[k] = {};
  823. if (!root[k]) {
  824. root[k] = {};
  825. }
  826. });
  827. root.accesskeys || (root.accesskeys = {});
  828. // create contextMenu items
  829. $.each(opt.items, function(key, item){
  830. var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ""),
  831. $label = null,
  832. $input = null;
  833. // iOS needs to see a click-event bound to an element to actually
  834. // have the TouchEvents infrastructure trigger the click event
  835. $t.on('click', $.noop);
  836. item.$node = $t.data({
  837. 'contextMenu': opt,
  838. 'contextMenuRoot': root,
  839. 'contextMenuKey': key
  840. });
  841. // register accesskey
  842. // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that
  843. if (item.accesskey) {
  844. var aks = splitAccesskey(item.accesskey);
  845. for (var i=0, ak; ak = aks[i]; i++) {
  846. if (!root.accesskeys[ak]) {
  847. root.accesskeys[ak] = item;
  848. item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>');
  849. break;
  850. }
  851. }
  852. }
  853. if (typeof item == "string") {
  854. $t.addClass('context-menu-separator not-selectable');
  855. } else if (item.type && types[item.type]) {
  856. // run custom type handler
  857. types[item.type].call($t, item, opt, root);
  858. // register commands
  859. $.each([opt, root], function(i,k){
  860. k.commands[key] = item;
  861. if ($.isFunction(item.callback)) {
  862. k.callbacks[key] = item.callback;
  863. }
  864. });
  865. } else {
  866. // add label for input
  867. if (item.type == 'html') {
  868. $t.addClass('context-menu-html not-selectable');
  869. } else if (item.type) {
  870. $label = $('<label></label>').appendTo($t);
  871. $('<span></span>').html(item._name || item.name).appendTo($label);
  872. $t.addClass('context-menu-input');
  873. opt.hasTypes = true;
  874. $.each([opt, root], function(i,k){
  875. k.commands[key] = item;
  876. k.inputs[key] = item;
  877. });
  878. } else if (item.items) {
  879. item.type = 'sub';
  880. }
  881. switch (item.type) {
  882. case 'text':
  883. $input = $('<input type="text" value="1" name="" value="">')
  884. .attr('name', 'context-menu-input-' + key)
  885. .val(item.value || "")
  886. .appendTo($label);
  887. break;
  888. case 'textarea':
  889. $input = $('<textarea name=""></textarea>')
  890. .attr('name', 'context-menu-input-' + key)
  891. .val(item.value || "")
  892. .appendTo($label);
  893. if (item.height) {
  894. $input.height(item.height);
  895. }
  896. break;
  897. case 'checkbox':
  898. $input = $('<input type="checkbox" value="1" name="" value="">')
  899. .attr('name', 'context-menu-input-' + key)
  900. .val(item.value || "")
  901. .prop("checked", !!item.selected)
  902. .prependTo($label);
  903. break;
  904. case 'radio':
  905. $input = $('<input type="radio" value="1" name="" value="">')
  906. .attr('name', 'context-menu-input-' + item.radio)
  907. .val(item.value || "")
  908. .prop("checked", !!item.selected)
  909. .prependTo($label);
  910. break;
  911. case 'select':
  912. $input = $('<select name="">')
  913. .attr('name', 'context-menu-input-' + key)
  914. .appendTo($label);
  915. if (item.options) {
  916. $.each(item.options, function(value, text) {
  917. $('<option></option>').val(value).text(text).appendTo($input);
  918. });
  919. $input.val(item.selected);
  920. }
  921. break;
  922. case 'sub':
  923. // FIXME: shouldn't this .html() be a .text()?
  924. $('<span></span>').html(item._name || item.name).appendTo($t);
  925. item.appendTo = item.$node;
  926. op.create(item, root);
  927. $t.data('contextMenu', item).addClass('context-menu-submenu');
  928. item.callback = null;
  929. break;
  930. case 'html':
  931. $(item.html).appendTo($t);
  932. break;
  933. default:
  934. $.each([opt, root], function(i,k){
  935. k.commands[key] = item;
  936. if ($.isFunction(item.callback)) {
  937. k.callbacks[key] = item.callback;
  938. }
  939. });
  940. // FIXME: shouldn't this .html() be a .text()?
  941. $('<span></span>').html(item._name || item.name || "").appendTo($t);
  942. break;
  943. }
  944. // disable key listener in <input>
  945. if (item.type && item.type != 'sub' && item.type != 'html') {
  946. $input
  947. .on('focus', handle.focusInput)
  948. .on('blur', handle.blurInput);
  949. if (item.events) {
  950. $input.on(item.events, opt);
  951. }
  952. }
  953. // add icons
  954. if (item.icon) {
  955. $t.addClass("icon icon-" + item.icon);
  956. }
  957. }
  958. // cache contained elements
  959. item.$input = $input;
  960. item.$label = $label;
  961. // attach item to menu
  962. $t.appendTo(opt.$menu);
  963. // Disable text selection
  964. if (!opt.hasTypes && $.support.eventSelectstart) {
  965. // browsers support user-select: none,
  966. // IE has a special event for text-selection
  967. // browsers supporting neither will not be preventing text-selection
  968. $t.on('selectstart.disableTextSelect', handle.abortevent);
  969. }
  970. });
  971. // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
  972. if (!opt.$node) {
  973. opt.$menu.css('display', 'none').addClass('context-menu-root');
  974. }
  975. opt.$menu.appendTo(opt.appendTo || document.body);
  976. },
  977. resize: function($menu, nested) {
  978. // determine widths of submenus, as CSS won't grow them automatically
  979. // position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100;
  980. // kinda sucks hard...
  981. // determine width of absolutely positioned element
  982. $menu.css({position: 'absolute', display: 'block'});
  983. // don't apply yet, because that would break nested elements' widths
  984. // add a pixel to circumvent word-break issue in IE9 - #80
  985. $menu.data('width', Math.ceil($menu.width()) + 1);
  986. // reset styles so they allow nested elements to grow/shrink naturally
  987. $menu.css({
  988. position: 'static',
  989. minWidth: '0px',
  990. maxWidth: '100000px'
  991. });
  992. // identify width of nested menus
  993. $menu.find('> li > ul').each(function() {
  994. op.resize($(this), true);
  995. });
  996. // reset and apply changes in the end because nested
  997. // elements' widths wouldn't be calculatable otherwise
  998. if (!nested) {
  999. $menu.find('ul').andSelf().css({
  1000. position: '',
  1001. display: '',
  1002. minWidth: '',
  1003. maxWidth: ''
  1004. }).width(function() {
  1005. return $(this).data('width');
  1006. });
  1007. }
  1008. },
  1009. update: function(opt, root) {
  1010. var $trigger = this;
  1011. if (root === undefined) {
  1012. root = opt;
  1013. op.resize(opt.$menu);
  1014. }
  1015. // re-check disabled for each item
  1016. opt.$menu.children().each(function(){
  1017. var $item = $(this),
  1018. key = $item.data('contextMenuKey'),
  1019. item = opt.items[key],
  1020. disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true;
  1021. // dis- / enable item
  1022. $item[disabled ? 'addClass' : 'removeClass']('disabled');
  1023. if (item.type) {
  1024. // dis- / enable input elements
  1025. $item.find('input, select, textarea').prop('disabled', disabled);
  1026. // update input states
  1027. switch (item.type) {
  1028. case 'text':
  1029. case 'textarea':
  1030. item.$input.val(item.value || "");
  1031. break;
  1032. case 'checkbox':
  1033. case 'radio':
  1034. item.$input.val(item.value || "").prop('checked', !!item.selected);
  1035. break;
  1036. case 'select':
  1037. item.$input.val(item.selected || "");
  1038. break;
  1039. }
  1040. }
  1041. if (item.$menu) {
  1042. // update sub-menu
  1043. op.update.call($trigger, item, root);
  1044. }
  1045. });
  1046. },
  1047. layer: function(opt, zIndex) {
  1048. // add transparent layer for click area
  1049. // filter and background for Internet Explorer, Issue #23
  1050. var $layer = opt.$layer = $('<div id="context-menu-layer" style="position:fixed; z-index:' + zIndex + '; top:0; left:0; opacity: 0; filter: alpha(opacity=0); background-color: #000;"></div>')
  1051. .css({height: $win.height(), width: $win.width(), display: 'block'})
  1052. .data('contextMenuRoot', opt)
  1053. .insertBefore(this)
  1054. .on('contextmenu', handle.abortevent)
  1055. .on('mousedown', handle.layerClick);
  1056. // IE6 doesn't know position:fixed;
  1057. if (!$.support.fixedPosition) {
  1058. $layer.css({
  1059. 'position' : 'absolute',
  1060. 'height' : $(document).height()
  1061. });
  1062. }
  1063. return $layer;
  1064. }
  1065. };
  1066. // split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key
  1067. function splitAccesskey(val) {
  1068. var t = val.split(/\s+/),
  1069. keys = [];
  1070. for (var i=0, k; k = t[i]; i++) {
  1071. k = k[0].toUpperCase(); // first character only
  1072. // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.
  1073. // a map to look up already used access keys would be nice
  1074. keys.push(k);
  1075. }
  1076. return keys;
  1077. }
  1078. // handle contextMenu triggers
  1079. $.fn.contextMenu = function(operation) {
  1080. if (operation === undefined) {
  1081. this.first().trigger('contextmenu');
  1082. } else if (operation.x && operation.y) {
  1083. this.first().trigger($.Event("contextmenu", {pageX: operation.x, pageY: operation.y}));
  1084. } else if (operation === "hide") {
  1085. var $menu = this.data('contextMenu').$menu;
  1086. $menu && $menu.trigger('contextmenu:hide');
  1087. } else if (operation === "destroy") {
  1088. $.contextMenu("destroy", {context: this});
  1089. } else if ($.isPlainObject(operation)) {
  1090. operation.context = this;
  1091. $.contextMenu("create", operation);
  1092. } else if (operation) {
  1093. this.removeClass('context-menu-disabled');
  1094. } else if (!operation) {
  1095. this.addClass('context-menu-disabled');
  1096. }
  1097. return this;
  1098. };
  1099. // manage contextMenu instances
  1100. $.contextMenu = function(operation, options) {
  1101. if (typeof operation != 'string') {
  1102. options = operation;
  1103. operation = 'create';
  1104. }
  1105. if (typeof options == 'string') {
  1106. options = {selector: options};
  1107. } else if (options === undefined) {
  1108. options = {};
  1109. }
  1110. // merge with default options
  1111. var o = $.extend(true, {}, defaults, options || {});
  1112. var $document = $(document);
  1113. var $context = $document;
  1114. var _hasContext = false;
  1115. if (!o.context || !o.context.length) {
  1116. o.context = document;
  1117. } else {
  1118. // you never know what they throw at you...
  1119. $context = $(o.context).first();
  1120. o.context = $context.get(0);
  1121. _hasContext = o.context !== document;
  1122. }
  1123. switch (operation) {
  1124. case 'create':
  1125. // no selector no joy
  1126. if (!o.selector) {
  1127. throw new Error('No selector specified');
  1128. }
  1129. // make sure internal classes are not bound to
  1130. if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {
  1131. throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');
  1132. }
  1133. if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
  1134. throw new Error('No Items sepcified');
  1135. }
  1136. counter ++;
  1137. o.ns = '.contextMenu' + counter;
  1138. if (!_hasContext) {
  1139. namespaces[o.selector] = o.ns;
  1140. }
  1141. menus[o.ns] = o;
  1142. // default to right click
  1143. if (!o.trigger) {
  1144. o.trigger = 'right';
  1145. }
  1146. if (!initialized) {
  1147. // make sure item click is registered first
  1148. $document
  1149. .on({
  1150. 'contextmenu:hide.contextMenu': handle.hideMenu,
  1151. 'prevcommand.contextMenu': handle.prevItem,
  1152. 'nextcommand.contextMenu': handle.nextItem,
  1153. 'contextmenu.contextMenu': handle.abortevent,
  1154. 'mouseenter.contextMenu': handle.menuMouseenter,
  1155. 'mouseleave.contextMenu': handle.menuMouseleave
  1156. }, '.context-menu-list')
  1157. .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)
  1158. .on({
  1159. 'mouseup.contextMenu': handle.itemClick,
  1160. 'contextmenu:focus.contextMenu': handle.focusItem,
  1161. 'contextmenu:blur.contextMenu': handle.blurItem,
  1162. 'contextmenu.contextMenu': handle.abortevent,
  1163. 'mouseenter.contextMenu': handle.itemMouseenter,
  1164. 'mouseleave.contextMenu': handle.itemMouseleave
  1165. }, '.context-menu-item');
  1166. initialized = true;
  1167. }
  1168. // engage native contextmenu event
  1169. $context
  1170. .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);
  1171. if (_hasContext) {
  1172. // add remove hook, just in case
  1173. $context.on('remove' + o.ns, function() {
  1174. $(this).contextMenu("destroy");
  1175. });
  1176. }
  1177. switch (o.trigger) {
  1178. case 'hover':
  1179. $context
  1180. .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
  1181. .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);
  1182. break;
  1183. case 'left':
  1184. $context.on('click' + o.ns, o.selector, o, handle.click);
  1185. break;
  1186. /*
  1187. default:
  1188. // http://www.quirksmode.org/dom/events/contextmenu.html
  1189. $document
  1190. .on('mousedown' + o.ns, o.selector, o, handle.mousedown)
  1191. .on('mouseup' + o.ns, o.selector, o, handle.mouseup);
  1192. break;
  1193. */
  1194. }
  1195. // create menu
  1196. if (!o.build) {
  1197. op.create(o);
  1198. }
  1199. break;
  1200. case 'destroy':
  1201. var $visibleMenu;
  1202. if (_hasContext) {
  1203. // get proper options
  1204. var context = o.context;
  1205. $.each(menus, function(ns, o) {
  1206. if (o.context !== context) {
  1207. return true;
  1208. }
  1209. $visibleMenu = $('.context-menu-list').filter(':visible');
  1210. if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) {
  1211. $visibleMenu.trigger('contextmenu:hide', {force: true});
  1212. }
  1213. try {
  1214. if (menus[o.ns].$menu) {
  1215. menus[o.ns].$menu.remove();
  1216. }
  1217. delete menus[o.ns];
  1218. } catch(e) {
  1219. menus[o.ns] = null;
  1220. }
  1221. $(o.context).off(o.ns);
  1222. return true;
  1223. });
  1224. } else if (!o.selector) {
  1225. $document.off('.contextMenu .contextMenuAutoHide');
  1226. $.each(menus, function(ns, o) {
  1227. $(o.context).off(o.ns);
  1228. });
  1229. namespaces = {};
  1230. menus = {};
  1231. counter = 0;
  1232. initialized = false;
  1233. $('#context-menu-layer, .context-menu-list').remove();
  1234. } else if (namespaces[o.selector]) {
  1235. $visibleMenu = $('.context-menu-list').filter(':visible');
  1236. if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) {
  1237. $visibleMenu.trigger('contextmenu:hide', {force: true});
  1238. }
  1239. try {
  1240. if (menus[namespaces[o.selector]].$menu) {
  1241. menus[namespaces[o.selector]].$menu.remove();
  1242. }
  1243. delete menus[namespaces[o.selector]];
  1244. } catch(e) {
  1245. menus[namespaces[o.selector]] = null;
  1246. }
  1247. $document.off(namespaces[o.selector]);
  1248. }
  1249. break;
  1250. case 'html5':
  1251. // if <command> or <menuitem> are not handled by the browser,
  1252. // or options was a bool true,
  1253. // initialize $.contextMenu for them
  1254. if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) {
  1255. $('menu[type="context"]').each(function() {
  1256. if (this.id) {
  1257. $.contextMenu({
  1258. selector: '[contextmenu=' + this.id +']',
  1259. items: $.contextMenu.fromMenu(this)
  1260. });
  1261. }
  1262. }).css('display', 'none');
  1263. }
  1264. break;
  1265. default:
  1266. throw new Error('Unknown operation "' + operation + '"');
  1267. }
  1268. return this;
  1269. };
  1270. // import values into <input> commands
  1271. $.contextMenu.setInputValues = function(opt, data) {
  1272. if (data === undefined) {
  1273. data = {};
  1274. }
  1275. $.each(opt.inputs, function(key, item) {
  1276. switch (item.type) {
  1277. case 'text':
  1278. case 'textarea':
  1279. item.value = data[key] || "";
  1280. break;
  1281. case 'checkbox':
  1282. item.selected = data[key] ? true : false;
  1283. break;
  1284. case 'radio':
  1285. item.selected = (data[item.radio] || "") == item.value ? true : false;
  1286. break;
  1287. case 'select':
  1288. item.selected = data[key] || "";
  1289. break;
  1290. }
  1291. });
  1292. };
  1293. // export values from <input> commands
  1294. $.contextMenu.getInputValues = function(opt, data) {
  1295. if (data === undefined) {
  1296. data = {};
  1297. }
  1298. $.each(opt.inputs, function(key, item) {
  1299. switch (item.type) {
  1300. case 'text':
  1301. case 'textarea':
  1302. case 'select':
  1303. data[key] = item.$input.val();
  1304. break;
  1305. case 'checkbox':
  1306. data[key] = item.$input.prop('checked');
  1307. break;
  1308. case 'radio':
  1309. if (item.$input.prop('checked')) {
  1310. data[item.radio] = item.value;
  1311. }
  1312. break;
  1313. }
  1314. });
  1315. return data;
  1316. };
  1317. // find <label for="xyz">
  1318. function inputLabel(node) {
  1319. return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;
  1320. }
  1321. // convert <menu> to items object
  1322. function menuChildren(items, $children, counter) {
  1323. if (!counter) {
  1324. counter = 0;
  1325. }
  1326. $children.each(function() {
  1327. var $node = $(this),
  1328. node = this,
  1329. nodeName = this.nodeName.toLowerCase(),
  1330. label,
  1331. item;
  1332. // extract <label><input>
  1333. if (nodeName == 'label' && $node.find('input, textarea, select').length) {
  1334. label = $node.text();
  1335. $node = $node.children().first();
  1336. node = $node.get(0);
  1337. nodeName = node.nodeName.toLowerCase();
  1338. }
  1339. /*
  1340. * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
  1341. * Not being the sadistic kind, $.contextMenu only accepts:
  1342. * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
  1343. * Everything else will be imported as an html node, which is not interfaced with contextMenu.
  1344. */
  1345. // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
  1346. switch (nodeName) {
  1347. // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
  1348. case 'menu':
  1349. item = {name: $node.attr('label'), items: {}};
  1350. counter = menuChildren(item.items, $node.children(), counter);
  1351. break;
  1352. // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
  1353. case 'a':
  1354. // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
  1355. case 'button':
  1356. item = {
  1357. name: $node.text(),
  1358. disabled: !!$node.attr('disabled'),
  1359. callback: (function(){ return function(){ $node.click(); }; })()
  1360. };
  1361. break;
  1362. // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command
  1363. case 'menuitem':
  1364. case 'command':
  1365. switch ($node.attr('type')) {
  1366. case undefined:
  1367. case 'command':
  1368. case 'menuitem':
  1369. item = {
  1370. name: $node.attr('label'),
  1371. disabled: !!$node.attr('disabled'),
  1372. callback: (function(){ return function(){ $node.click(); }; })()
  1373. };
  1374. break;
  1375. case 'checkbox':
  1376. item = {
  1377. type: 'checkbox',
  1378. disabled: !!$node.attr('disabled'),
  1379. name: $node.attr('label'),
  1380. selected: !!$node.attr('checked')
  1381. };
  1382. break;
  1383. case 'radio':
  1384. item = {
  1385. type: 'radio',
  1386. disabled: !!$node.attr('disabled'),
  1387. name: $node.attr('label'),
  1388. radio: $node.attr('radiogroup'),
  1389. value: $node.attr('id'),
  1390. selected: !!$node.attr('checked')
  1391. };
  1392. break;
  1393. default:
  1394. item = undefined;
  1395. }
  1396. break;
  1397. case 'hr':
  1398. item = '-------';
  1399. break;
  1400. case 'input':
  1401. switch ($node.attr('type')) {
  1402. case 'text':
  1403. item = {
  1404. type: 'text',
  1405. name: label || inputLabel(node),
  1406. disabled: !!$node.attr('disabled'),
  1407. value: $node.val()
  1408. };
  1409. break;
  1410. case 'checkbox':
  1411. item = {
  1412. type: 'checkbox',
  1413. name: label || inputLabel(node),
  1414. disabled: !!$node.attr('disabled'),
  1415. selected: !!$node.attr('checked')
  1416. };
  1417. break;
  1418. case 'radio':
  1419. item = {
  1420. type: 'radio',
  1421. name: label || inputLabel(node),
  1422. disabled: !!$node.attr('disabled'),
  1423. radio: !!$node.attr('name'),
  1424. value: $node.val(),
  1425. selected: !!$node.attr('checked')
  1426. };
  1427. break;
  1428. default:
  1429. item = undefined;
  1430. break;
  1431. }
  1432. break;
  1433. case 'select':
  1434. item = {
  1435. type: 'select',
  1436. name: label || inputLabel(node),
  1437. disabled: !!$node.attr('disabled'),
  1438. selected: $node.val(),
  1439. options: {}
  1440. };
  1441. $node.children().each(function(){
  1442. item.options[this.value] = $(this).text();
  1443. });
  1444. break;
  1445. case 'textarea':
  1446. item = {
  1447. type: 'textarea',
  1448. name: label || inputLabel(node),
  1449. disabled: !!$node.attr('disabled'),
  1450. value: $node.val()
  1451. };
  1452. break;
  1453. case 'label':
  1454. break;
  1455. default:
  1456. item = {type: 'html', html: $node.clone(true)};
  1457. break;
  1458. }
  1459. if (item) {
  1460. counter++;
  1461. items['key' + counter] = item;
  1462. }
  1463. });
  1464. return counter;
  1465. }
  1466. // convert html5 menu
  1467. $.contextMenu.fromMenu = function(element) {
  1468. var $this = $(element),
  1469. items = {};
  1470. menuChildren(items, $this.children());
  1471. return items;
  1472. };
  1473. // make defaults accessible
  1474. $.contextMenu.defaults = defaults;
  1475. $.contextMenu.types = types;
  1476. // export internal functions - undocumented, for hacking only!
  1477. $.contextMenu.handle = handle;
  1478. $.contextMenu.op = op;
  1479. $.contextMenu.menus = menus;
  1480. })(jQuery);