jquery_context_menu-rails4 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ OTEyYmFmMzZjZjFmMWIyYzJjODk2YzdlMzBhYjRiNjM1OGExZGJiNg==
5
+ data.tar.gz: !binary |-
6
+ ZmVmODQ5ZDc0NGY1YTM0MDdlOGU1MTI4OTVlMDQ3NDg2MGY1ZjdiZg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NWFhMjRmMzU1Y2ExNzUzNTM5MWFlZDE1ZGRhZjczYzkxYmM1YWY0MzVhNDhh
10
+ ZDQwNjIwMjE2NjA3YTU3ZDE1ZTU1NzQ2MTE4MDZkNzQ2OTY4NWRkNjg0YjBj
11
+ MTYyNWE3ZjBhMTM4NWQ2MTc1Yzk1YmExOWUxNmNiYTcxNjc3ZTU=
12
+ data.tar.gz: !binary |-
13
+ NDFjMzViMTc2NmIyYjI0MGJmNGQ4NWYyOGY4NmI2ODIwYjhjMjZiZjY2ZDNl
14
+ OThlMzIzMDU4ZmRkNzZjZTkwZDVhZjdhYjhiOWMwOWQwOTliMDc5YzBmY2Fh
15
+ YjUwZTk1N2UzYWI2ZTFjY2I1NDEwZTIyOWQwNGY4NDJiZWEyYzI=
@@ -0,0 +1 @@
1
+ jquery_context_menu-rails-0.0.1.gem
@@ -0,0 +1,25 @@
1
+ # Ruby Gem for jQuery contextMenu Plug-In #
2
+
3
+ This gem wraps the jQuery context menu plug-in (https://github.com/medialize/jQuery-contextMenu) for convenient use in Rails 3. jQuery-contextMenu is a jQuery plug-in that pops up a menu when the user right clicks on an element to which the menu is assigned.
4
+
5
+ ## Usage ##
6
+
7
+ To include the plug-in in your rails application, modify your Gemfile to includ the following:
8
+
9
+ gem "jquery_context_menu-rails"
10
+
11
+ Next add the following line to app/assets/javascripts/application.js:
12
+
13
+ //= require jquery.contextMenu.js
14
+
15
+ And, in app/assets/stylesheets/application.css.scss add:
16
+
17
+ //= require jquery.contextMenu.css
18
+
19
+ These additions will make the jQuery-contextMenu plug-in available on all pages. If you want to limit it to a subset of pages, require the .js and .css files in the appropriate Rails Javascript and CSS files.
20
+
21
+ For documentation on how to use the context menu, please see the original jQuery-contextMenu site.
22
+
23
+ ## License ##
24
+
25
+ $.contextMenu is published under the [MIT license](http://www.opensource.org/licenses/mit-license) and [GPL v3](http://opensource.org/licenses/GPL-3.0), as is this package.
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jquery_context_menu/rails/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "jquery_context_menu-rails4"
8
+ s.version = JQueryContextMenu::Rails::VERSION
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["Harssh Sshrivastava","Bill Dieter"]
11
+ s.email = ["harssh122@gmail.com","dieter@acm.org"]
12
+ s.homepage = "https://github.com/harssh/jquery_context_menu-rails"
13
+ s.summary = "Use jQuery-contextMenu with Rails 3 and 4"
14
+ s.description = "This gem provides jQuery-contextMenufor your Rails application. (jQuery-contextMenu source code is at https://github.com/medialize/jQuery-contextMenu.git)"
15
+ s.license = "MIT"
16
+
17
+ s.add_development_dependency "bundler", "~> 1.3"
18
+ s.add_development_dependency "rake"
19
+
20
+
21
+ s.add_dependency "railties", ">= 3.2.0", "< 5.0"
22
+ s.add_dependency "jquery-rails"
23
+ s.add_dependency 'jquery-ui-rails'
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.executables = `git ls-files`.split("\n").select{|f| f =~ /^bin/}
27
+ s.require_path = 'lib'
28
+ end
@@ -0,0 +1 @@
1
+ require 'jquery_context_menu/rails'
@@ -0,0 +1,6 @@
1
+ module JQqueryContextMenu
2
+ module Rails
3
+ require 'jquery_context_menu/rails/engine'
4
+ require 'jquery_context_menu/rails/version'
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module JQueryContextMenu
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module JQueryContextMenu
2
+ module Rails
3
+ VERSION = "1.0.0"
4
+ JQUERY_CONTEXTMENU_VERSION = "1.10.0"
5
+ end
6
+ end
@@ -0,0 +1,1686 @@
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("icon icon-" + 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
+
1686
+ })(jQuery);