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