jquery_context_menu-rails4 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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);