~holger-seelig/cobweb.js/trunk

« back to all changes in this revision

Viewing changes to cobweb.js/lib/jquery-contextMenu/src/jquery.contextMenu.js

  • Committer: Holger Seelig
  • Date: 2017-08-22 04:53:24 UTC
  • Revision ID: holger.seelig@yahoo.de-20170822045324-4of4xxgt79669gbt
Switched to npm.

Show diffs side-by-side

added added

removed removed

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