jquery_context_menu-rails 0.0.4

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