jquery_context_menu-rails 0.0.4

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