User:DarTar/wg system.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*!
 * jQuery contextMenu - Plugin for simple contextMenu handling
 *
 * Version: 1.6.6
 *
 * Authors: Rodney Rehm, Addy Osmani (patches for FF)
 * Web: http://medialize.github.com/jQuery-contextMenu/
 *
 * Licensed under
 *   MIT License http://www.opensource.org/licenses/mit-license
 *   GPL v3 http://opensource.org/licenses/GPL-3.0
 *
 */
// <syntaxhighlight lang=javascript>
(function($, undefined){
    
    // TODO: -
        // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
        // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative

// determine html5 compatibility
$.support.htmlMenuitem = ('HTMLMenuItemElement' in window);
$.support.htmlCommand = ('HTMLCommandElement' in window);
$.support.eventSelectstart = ("onselectstart" in document.documentElement);
/* // should the need arise, test for css user-select
$.support.cssUserSelect = (function(){
    var t = false,
        e = document.createElement('div');
    
    $.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) {
        var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect',
            prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select';
            
        e.style.cssText = prop + ': text;';
        if (e.style[propCC] == 'text') {
            t = true;
            return false;
        }
        
        return true;
    });
    
    return t;
})();
*/

if (!$.ui || !$.ui.widget) {
    // duck punch $.cleanData like jQueryUI does to get that remove event
    // https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js#L16-24
    var _cleanData = $.cleanData;
    $.cleanData = function( elems ) {
        for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
            try {
                $( elem ).triggerHandler( "remove" );
                // http://bugs.jquery.com/ticket/8235
            } catch( e ) {}
        }
        _cleanData( elems );
    };
}

var // currently active contextMenu trigger
    $currentTrigger = null,
    // is contextMenu initialized with at least one menu?
    initialized = false,
    // window handle
    $win = $(window),
    // number of registered menus
    counter = 0,
    // mapping selector to namespace
    namespaces = {},
    // mapping namespace to options
    menus = {},
    // custom command type handlers
    types = {},
    // default values
    defaults = {
        // selector of contextMenu trigger
        selector: null,
        // where to append the menu to
        appendTo: null,
        // method to trigger context menu ["right", "left", "hover"]
        trigger: "right",
        // hide menu when mouse leaves trigger / menu elements
        autoHide: false,
        // ms to wait before showing a hover-triggered context menu
        delay: 200,
        // flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu
        // as long as the trigger happened on one of the trigger-element's child nodes
        reposition: true,
        // determine position to show menu at
        determinePosition: function($menu) {
            // position to the lower middle of the trigger element
            if ($.ui && $.ui.position) {
                // .position() is provided as a jQuery UI utility
                // (...and it won't work on hidden elements)
                $menu.css('display', 'block').position({
                    my: "center top",
                    at: "center bottom",
                    of: this,
                    offset: "0 5",
                    collision: "fit"
                }).css('display', 'none');
            } else {
                // determine contextMenu position
                var offset = this.offset();
                offset.top += this.outerHeight();
                offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;
                $menu.css(offset);
            }
        },
        // position menu
        position: function(opt, x, y) {
            var $this = this,
                offset;
            // determine contextMenu position
            if (!x && !y) {
                opt.determinePosition.call(this, opt.$menu);
                return;
            } else if (x === "maintain" && y === "maintain") {
                // x and y must not be changed (after re-show on command click)
                offset = opt.$menu.position();
            } else {
                // x and y are given (by mouse event)
                offset = {top: y, left: x};
            }
            
            // correct offset if viewport demands it
            var bottom = $win.scrollTop() + $win.height(),
                right = $win.scrollLeft() + $win.width(),
                height = opt.$menu.height(),
                width = opt.$menu.width();
            
            if (offset.top + height > bottom) {
                offset.top -= height;
            }
            
            if (offset.left + width > right) {
                offset.left -= width;
            }
            
            opt.$menu.css(offset);
        },
        // position the sub-menu
        positionSubmenu: function($menu) {
            if ($.ui && $.ui.position) {
                // .position() is provided as a jQuery UI utility
                // (...and it won't work on hidden elements)
                $menu.css('display', 'block').position({
                    my: "left top",
                    at: "right top",
                    of: this,
                    collision: "flipfit fit"
                }).css('display', '');
            } else {
                // determine contextMenu position
                var offset = {
                    top: 0,
                    left: this.outerWidth()
                };
                $menu.css(offset);
            }
        },
        // offset to add to zIndex
        zIndex: 1,
        // show hide animation settings
        animation: {
            duration: 50,
            show: 'slideDown',
            hide: 'slideUp'
        },
        // events
        events: {
            show: $.noop,
            hide: $.noop
        },
        // default callback
        callback: null,
        // list of contextMenu items
        items: {}
    },
    // mouse position for hover activation
    hoveract = {
        timer: null,
        pageX: null,
        pageY: null
    },
    // determine zIndex
    zindex = function($t) {
        var zin = 0,
            $tt = $t;

        while (true) {
            zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);
            $tt = $tt.parent();
            if (!$tt || !$tt.length || "html body".indexOf($tt.prop('nodeName').toLowerCase()) > -1 ) {
                break;
            }
        }
        
        return zin;
    },
    // event handlers
    handle = {
        // abort anything
        abortevent: function(e){
            e.preventDefault();
            e.stopImmediatePropagation();
        },
        
        // contextmenu show dispatcher
        contextmenu: function(e) {
            var $this = $(this);
            
            // disable actual context-menu
            e.preventDefault();
            e.stopImmediatePropagation();
            
            // abort native-triggered events unless we're triggering on right click
            if (e.data.trigger != 'right' && e.originalEvent) {
                return;
            }
            
            // abort event if menu is visible for this trigger
            if ($this.hasClass('context-menu-active')) {
                return;
            }
            
            if (!$this.hasClass('context-menu-disabled')) {
                // theoretically need to fire a show event at <menu>
                // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
                // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });
                // e.data.$menu.trigger(evt);
                
                $currentTrigger = $this;
                if (e.data.build) {
                    var built = e.data.build($currentTrigger, e);
                    // abort if build() returned false
                    if (built === false) {
                        return;
                    }
                    
                    // dynamically build menu on invocation
                    e.data = $.extend(true, {}, defaults, e.data, built || {});

                    // abort if there are no items to display
                    if (!e.data.items || $.isEmptyObject(e.data.items)) {
                        // Note: jQuery captures and ignores errors from event handlers
                        if (window.console) {
                            (console.error || console.log)("No items specified to show in contextMenu");
                        }
                        
                        throw new Error('No Items specified');
                    }
                    
                    // backreference for custom command type creation
                    e.data.$trigger = $currentTrigger;
                    
                    op.create(e.data);
                }
                // show menu
                op.show.call($this, e.data, e.pageX, e.pageY);
            }
        },
        // contextMenu left-click trigger
        click: function(e) {
            e.preventDefault();
            e.stopImmediatePropagation();
            $(this).trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
        },
        // contextMenu right-click trigger
        mousedown: function(e) {
            // register mouse down
            var $this = $(this);
            
            // hide any previous menus
            if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {
                $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');
            }
            
            // activate on right click
            if (e.button == 2) {
                $currentTrigger = $this.data('contextMenuActive', true);
            }
        },
        // contextMenu right-click trigger
        mouseup: function(e) {
            // show menu
            var $this = $(this);
            if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {
                e.preventDefault();
                e.stopImmediatePropagation();
                $currentTrigger = $this;
                $this.trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
            }
            
            $this.removeData('contextMenuActive');
        },
        // contextMenu hover trigger
        mouseenter: function(e) {
            var $this = $(this),
                $related = $(e.relatedTarget),
                $document = $(document);
            
            // abort if we're coming from a menu
            if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
                return;
            }
            
            // abort if a menu is shown
            if ($currentTrigger && $currentTrigger.length) {
                return;
            }
            
            hoveract.pageX = e.pageX;
            hoveract.pageY = e.pageY;
            hoveract.data = e.data;
            $document.on('mousemove.contextMenuShow', handle.mousemove);
            hoveract.timer = setTimeout(function() {
                hoveract.timer = null;
                $document.off('mousemove.contextMenuShow');
                $currentTrigger = $this;
                $this.trigger($.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY }));
            }, e.data.delay );
        },
        // contextMenu hover trigger
        mousemove: function(e) {
            hoveract.pageX = e.pageX;
            hoveract.pageY = e.pageY;
        },
        // contextMenu hover trigger
        mouseleave: function(e) {
            // abort if we're leaving for a menu
            var $related = $(e.relatedTarget);
            if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
                return;
            }
            
            try {
                clearTimeout(hoveract.timer);
            } catch(e) {}
            
            hoveract.timer = null;
        },
        
        // click on layer to hide contextMenu
        layerClick: function(e) {
            var $this = $(this),
                root = $this.data('contextMenuRoot'),
                mouseup = false,
                button = e.button,
                x = e.pageX,
                y = e.pageY,
                target,
                offset,
                selectors;
                
            e.preventDefault();
            e.stopImmediatePropagation();
            
            setTimeout(function() {
                var $window, hideshow, possibleTarget;
                var triggerAction = ((root.trigger == 'left' && button === 0) || (root.trigger == 'right' && button === 2));
                
                // find the element that would've been clicked, wasn't the layer in the way
                if (document.elementFromPoint) {
                    root.$layer.hide();
                    target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop());
                    root.$layer.show();
                }
                
                if (root.reposition && triggerAction) {
                    if (document.elementFromPoint) {
                        if (root.$trigger.is(target) || root.$trigger.has(target).length) {
                            root.position.call(root.$trigger, root, x, y);
                            return;
                        }
                    } else {
                        offset = root.$trigger.offset();
                        $window = $(window);
                        // while this looks kinda awful, it's the best way to avoid
                        // unnecessarily calculating any positions
                        offset.top += $window.scrollTop();
                        if (offset.top <= e.pageY) {
                            offset.left += $window.scrollLeft();
                            if (offset.left <= e.pageX) {
                                offset.bottom = offset.top + root.$trigger.outerHeight();
                                if (offset.bottom >= e.pageY) {
                                    offset.right = offset.left + root.$trigger.outerWidth();
                                    if (offset.right >= e.pageX) {
                                        // reposition
                                        root.position.call(root.$trigger, root, x, y);
                                        return;
                                    }
                                }
                            }
                        }
                    }
                }
                
                if (target && triggerAction) {
                    root.$trigger.one('contextmenu:hidden', function() {
                        $(target).contextMenu({x: x, y: y});
                    });
                }

                root.$menu.trigger('contextmenu:hide');
            }, 50);
        },
        // key handled :hover
        keyStop: function(e, opt) {
            if (!opt.isInput) {
                e.preventDefault();
            }
            
            e.stopPropagation();
        },
        key: function(e) {
            var opt = $currentTrigger.data('contextMenu') || {};

            switch (e.keyCode) {
                case 9:
                case 38: // up
                    handle.keyStop(e, opt);
                    // if keyCode is [38 (up)] or [9 (tab) with shift]
                    if (opt.isInput) {
                        if (e.keyCode == 9 && e.shiftKey) {
                            e.preventDefault();
                            opt.$selected && opt.$selected.find('input, textarea, select').blur();
                            opt.$menu.trigger('prevcommand');
                            return;
                        } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
                            // checkboxes don't capture this key
                            e.preventDefault();
                            return;
                        }
                    } else if (e.keyCode != 9 || e.shiftKey) {
                        opt.$menu.trigger('prevcommand');
                        return;
                    }
                    // omitting break;
                    
                // case 9: // tab - reached through omitted break;
                case 40: // down
                    handle.keyStop(e, opt);
                    if (opt.isInput) {
                        if (e.keyCode == 9) {
                            e.preventDefault();
                            opt.$selected && opt.$selected.find('input, textarea, select').blur();
                            opt.$menu.trigger('nextcommand');
                            return;
                        } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
                            // checkboxes don't capture this key
                            e.preventDefault();
                            return;
                        }
                    } else {
                        opt.$menu.trigger('nextcommand');
                        return;
                    }
                    break;
                
                case 37: // left
                    handle.keyStop(e, opt);
                    if (opt.isInput || !opt.$selected || !opt.$selected.length) {
                        break;
                    }
                
                    if (!opt.$selected.parent().hasClass('context-menu-root')) {
                        var $parent = opt.$selected.parent().parent();
                        opt.$selected.trigger('contextmenu:blur');
                        opt.$selected = $parent;
                        return;
                    }
                    break;
                    
                case 39: // right
                    handle.keyStop(e, opt);
                    if (opt.isInput || !opt.$selected || !opt.$selected.length) {
                        break;
                    }
                    
                    var itemdata = opt.$selected.data('contextMenu') || {};
                    if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {
                        opt.$selected = null;
                        itemdata.$selected = null;
                        itemdata.$menu.trigger('nextcommand');
                        return;
                    }
                    break;
                
                case 35: // end
                case 36: // home
                    if (opt.$selected && opt.$selected.find('input, textarea, select').length) {
                        return;
                    } else {
                        (opt.$selected && opt.$selected.parent() || opt.$menu)
                            .children(':not(.disabled, .not-selectable)')[e.keyCode == 36 ? 'first' : 'last']()
                            .trigger('contextmenu:focus');
                        e.preventDefault();
                        return;
                    }
                    break;
                    
                case 13: // enter
                    handle.keyStop(e, opt);
                    if (opt.isInput) {
                        if (opt.$selected && !opt.$selected.is('textarea, select')) {
                            e.preventDefault();
                            return;
                        }
                        break;
                    }
                    opt.$selected && opt.$selected.trigger('mouseup');
                    return;
                    
                case 32: // space
                case 33: // page up
                case 34: // page down
                    // prevent browser from scrolling down while menu is visible
                    handle.keyStop(e, opt);
                    return;
                    
                case 27: // esc
                    handle.keyStop(e, opt);
                    opt.$menu.trigger('contextmenu:hide');
                    return;
                    
                default: // 0-9, a-z
                    var k = (String.fromCharCode(e.keyCode)).toUpperCase();
                    if (opt.accesskeys[k]) {
                        // according to the specs accesskeys must be invoked immediately
                        opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu
                            ? 'contextmenu:focus'
                            : 'mouseup'
                        );
                        return;
                    }
                    break;
            }
            // pass event to selected item,
            // stop propagation to avoid endless recursion
            e.stopPropagation();
            opt.$selected && opt.$selected.trigger(e);
        },

        // select previous possible command in menu
        prevItem: function(e) {
            e.stopPropagation();
            var opt = $(this).data('contextMenu') || {};

            // obtain currently selected menu
            if (opt.$selected) {
                var $s = opt.$selected;
                opt = opt.$selected.parent().data('contextMenu') || {};
                opt.$selected = $s;
            }
            
            var $children = opt.$menu.children(),
                $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
                $round = $prev;
            
            // skip disabled
            while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {
                if ($prev.prev().length) {
                    $prev = $prev.prev();
                } else {
                    $prev = $children.last();
                }
                if ($prev.is($round)) {
                    // break endless loop
                    return;
                }
            }
            
            // leave current
            if (opt.$selected) {
                handle.itemMouseleave.call(opt.$selected.get(0), e);
            }
            
            // activate next
            handle.itemMouseenter.call($prev.get(0), e);
            
            // focus input
            var $input = $prev.find('input, textarea, select');
            if ($input.length) {
                $input.focus();
            }
        },
        // select next possible command in menu
        nextItem: function(e) {
            e.stopPropagation();
            var opt = $(this).data('contextMenu') || {};

            // obtain currently selected menu
            if (opt.$selected) {
                var $s = opt.$selected;
                opt = opt.$selected.parent().data('contextMenu') || {};
                opt.$selected = $s;
            }

            var $children = opt.$menu.children(),
                $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
                $round = $next;

            // skip disabled
            while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {
                if ($next.next().length) {
                    $next = $next.next();
                } else {
                    $next = $children.first();
                }
                if ($next.is($round)) {
                    // break endless loop
                    return;
                }
            }
            
            // leave current
            if (opt.$selected) {
                handle.itemMouseleave.call(opt.$selected.get(0), e);
            }
            
            // activate next
            handle.itemMouseenter.call($next.get(0), e);
            
            // focus input
            var $input = $next.find('input, textarea, select');
            if ($input.length) {
                $input.focus();
            }
        },
        
        // flag that we're inside an input so the key handler can act accordingly
        focusInput: function(e) {
            var $this = $(this).closest('.context-menu-item'),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;

            root.$selected = opt.$selected = $this;
            root.isInput = opt.isInput = true;
        },
        // flag that we're inside an input so the key handler can act accordingly
        blurInput: function(e) {
            var $this = $(this).closest('.context-menu-item'),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;

            root.isInput = opt.isInput = false;
        },
        
        // :hover on menu
        menuMouseenter: function(e) {
            var root = $(this).data().contextMenuRoot;
            root.hovering = true;
        },
        // :hover on menu
        menuMouseleave: function(e) {
            var root = $(this).data().contextMenuRoot;
            if (root.$layer && root.$layer.is(e.relatedTarget)) {
                root.hovering = false;
            }
        },
        
        // :hover done manually so key handling is possible
        itemMouseenter: function(e) {
            var $this = $(this),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;
            
            root.hovering = true;

            // abort if we're re-entering
            if (e && root.$layer && root.$layer.is(e.relatedTarget)) {
                e.preventDefault();
                e.stopImmediatePropagation();
            }

            // make sure only one item is selected
            (opt.$menu ? opt : root).$menu
                .children('.hover').trigger('contextmenu:blur');

            if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {
                opt.$selected = null;
                return;
            }
            
            $this.trigger('contextmenu:focus');
        },
        // :hover done manually so key handling is possible
        itemMouseleave: function(e) {
            var $this = $(this),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;

            if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {
                root.$selected && root.$selected.trigger('contextmenu:blur');
                e.preventDefault();
                e.stopImmediatePropagation();
                root.$selected = opt.$selected = opt.$node;
                return;
            }
            
            $this.trigger('contextmenu:blur');
        },
        // contextMenu item click
        itemClick: function(e) {
            var $this = $(this),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot,
                key = data.contextMenuKey,
                callback;

            // abort if the key is unknown or disabled or is a menu
            if (!opt.items[key] || $this.is('.disabled, .context-menu-submenu, .context-menu-separator, .not-selectable')) {
                return;
            }

            e.preventDefault();
            e.stopImmediatePropagation();

            if ($.isFunction(root.callbacks[key]) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) {
                // item-specific callback
                callback = root.callbacks[key];
            } else if ($.isFunction(root.callback)) {
                // default callback
                callback = root.callback;
            } else {
                // no callback, no action
                return;
            }

            // hide menu if callback doesn't stop that
            if (callback.call(root.$trigger, key, root) !== false) {
                root.$menu.trigger('contextmenu:hide');
            } else if (root.$menu.parent().length) {
                op.update.call(root.$trigger, root);
            }
        },
        // ignore click events on input elements
        inputClick: function(e) {
            e.stopImmediatePropagation();
        },
        
        // hide <menu>
        hideMenu: function(e, data) {
            var root = $(this).data('contextMenuRoot');
            op.hide.call(root.$trigger, root, data && data.force);
        },
        // focus <command>
        focusItem: function(e) {
            e.stopPropagation();
            var $this = $(this),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;

            $this.addClass('hover')
                .siblings('.hover').trigger('contextmenu:blur');
            
            // remember selected
            opt.$selected = root.$selected = $this;
            
            // position sub-menu - do after show so dumb $.ui.position can keep up
            if (opt.$node) {
                root.positionSubmenu.call(opt.$node, opt.$menu);
            }
        },
        // blur <command>
        blurItem: function(e) {
            e.stopPropagation();
            var $this = $(this),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;
            
            $this.removeClass('hover');
            opt.$selected = null;
        }
    },
    // operations
    op = {
        show: function(opt, x, y) {
            var $trigger = $(this),
                offset,
                css = {};

            // hide any open menus
            $('#context-menu-layer').trigger('mousedown');

            // backreference for callbacks
            opt.$trigger = $trigger;

            // show event
            if (opt.events.show.call($trigger, opt) === false) {
                $currentTrigger = null;
                return;
            }

            // create or update context menu
            op.update.call($trigger, opt);
            
            // position menu
            opt.position.call($trigger, opt, x, y);

            // make sure we're in front
            if (opt.zIndex) {
                css.zIndex = zindex($trigger) + opt.zIndex;
            }
            
            // add layer
            op.layer.call(opt.$menu, opt, css.zIndex);
            
            // adjust sub-menu zIndexes
            opt.$menu.find('ul').css('zIndex', css.zIndex + 1);
            
            // position and show context menu
            opt.$menu.css( css )[opt.animation.show](opt.animation.duration, function() {
                $trigger.trigger('contextmenu:visible');
            });
            // make options available and set state
            $trigger
                .data('contextMenu', opt)
                .addClass("context-menu-active");
            
            // register key handler
            $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);
            // register autoHide handler
            if (opt.autoHide) {
                // mouse position handler
                $(document).on('mousemove.contextMenuAutoHide', function(e) {
                    // need to capture the offset on mousemove,
                    // since the page might've been scrolled since activation
                    var pos = $trigger.offset();
                    pos.right = pos.left + $trigger.outerWidth();
                    pos.bottom = pos.top + $trigger.outerHeight();
                    
                    if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {
                        // if mouse in menu...
                        opt.$menu.trigger('contextmenu:hide');
                    }
                });
            }
        },
        hide: function(opt, force) {
            var $trigger = $(this);
            if (!opt) {
                opt = $trigger.data('contextMenu') || {};
            }
            
            // hide event
            if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) {
                return;
            }
            
            // remove options and revert state
            $trigger
                .removeData('contextMenu')
                .removeClass("context-menu-active");
            
            if (opt.$layer) {
                // keep layer for a bit so the contextmenu event can be aborted properly by opera
                setTimeout((function($layer) {
                    return function(){
                        $layer.remove();
                    };
                })(opt.$layer), 10);
                
                try {
                    delete opt.$layer;
                } catch(e) {
                    opt.$layer = null;
                }
            }
            
            // remove handle
            $currentTrigger = null;
            // remove selected
            opt.$menu.find('.hover').trigger('contextmenu:blur');
            opt.$selected = null;
            // unregister key and mouse handlers
            //$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705
            $(document).off('.contextMenuAutoHide').off('keydown.contextMenu');
            // hide menu
            opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration, function (){
                // tear down dynamically built menu after animation is completed.
                if (opt.build) {
                    opt.$menu.remove();
                    $.each(opt, function(key, value) {
                        switch (key) {
                            case 'ns':
                            case 'selector':
                            case 'build':
                            case 'trigger':
                                return true;

                            default:
                                opt[key] = undefined;
                                try {
                                    delete opt[key];
                                } catch (e) {}
                                return true;
                        }
                    });
                }
                
                setTimeout(function() {
                    $trigger.trigger('contextmenu:hidden');
                }, 10);
            });
        },
        create: function(opt, root) {
            if (root === undefined) {
                root = opt;
            }
            // create contextMenu
            opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || "").data({
                'contextMenu': opt,
                'contextMenuRoot': root
            });
            
            $.each(['callbacks', 'commands', 'inputs'], function(i,k){
                opt[k] = {};
                if (!root[k]) {
                    root[k] = {};
                }
            });
            
            root.accesskeys || (root.accesskeys = {});
            
            // create contextMenu items
            $.each(opt.items, function(key, item){
                var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ""),
                    $label = null,
                    $input = null;
                
                // iOS needs to see a click-event bound to an element to actually
                // have the TouchEvents infrastructure trigger the click event
                $t.on('click', $.noop);
                
                item.$node = $t.data({
                    'contextMenu': opt,
                    'contextMenuRoot': root,
                    'contextMenuKey': key
                });
                
                // register accesskey
                // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that
                if (item.accesskey) {
                    var aks = splitAccesskey(item.accesskey);
                    for (var i=0, ak; ak = aks[i]; i++) {
                        if (!root.accesskeys[ak]) {
                            root.accesskeys[ak] = item;
                            item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>');
                            break;
                        }
                    }
                }
                
                if (typeof item == "string") {
                    $t.addClass('context-menu-separator not-selectable');
                } else if (item.type && types[item.type]) {
                    // run custom type handler
                    types[item.type].call($t, item, opt, root);
                    // register commands
                    $.each([opt, root], function(i,k){
                        k.commands[key] = item;
                        if ($.isFunction(item.callback)) {
                            k.callbacks[key] = item.callback;
                        }
                    });
                } else {
                    // add label for input
                    if (item.type == 'html') {
                        $t.addClass('context-menu-html not-selectable');
                    } else if (item.type) {
                        $label = $('<label></label>').appendTo($t);
                        $('<span></span>').html(item._name || item.name).appendTo($label);
                        $t.addClass('context-menu-input');
                        opt.hasTypes = true;
                        $.each([opt, root], function(i,k){
                            k.commands[key] = item;
                            k.inputs[key] = item;
                        });
                    } else if (item.items) {
                        item.type = 'sub';
                    }
                
                    switch (item.type) {
                        case 'text':
                            $input = $('<input type="text" value="1" name="" value="">')
                                .attr('name', 'context-menu-input-' + key)
                                .val(item.value || "")
                                .appendTo($label);
                            break;
                    
                        case 'textarea':
                            $input = $('<textarea name=""></textarea>')
                                .attr('name', 'context-menu-input-' + key)
                                .val(item.value || "")
                                .appendTo($label);

                            if (item.height) {
                                $input.height(item.height);
                            }
                            break;

                        case 'checkbox':
                            $input = $('<input type="checkbox" value="1" name="" value="">')
                                .attr('name', 'context-menu-input-' + key)
                                .val(item.value || "")
                                .prop("checked", !!item.selected)
                                .prependTo($label);
                            break;

                        case 'radio':
                            $input = $('<input type="radio" value="1" name="" value="">')
                                .attr('name', 'context-menu-input-' + item.radio)
                                .val(item.value || "")
                                .prop("checked", !!item.selected)
                                .prependTo($label);
                            break;
                    
                        case 'select':
                            $input = $('<select name="">')
                                .attr('name', 'context-menu-input-' + key)
                                .appendTo($label);
                            if (item.options) {
                                $.each(item.options, function(value, text) {
                                    $('<option></option>').val(value).text(text).appendTo($input);
                                });
                                $input.val(item.selected);
                            }
                            break;
                        
                        case 'sub':
                            // FIXME: shouldn't this .html() be a .text()?
                            $('<span></span>').html(item._name || item.name).appendTo($t);
                            item.appendTo = item.$node;
                            op.create(item, root);
                            $t.data('contextMenu', item).addClass('context-menu-submenu');
                            item.callback = null;
                            break;
                        
                        case 'html':
                            $(item.html).appendTo($t);
                            break;
                        
                        default:
                            $.each([opt, root], function(i,k){
                                k.commands[key] = item;
                                if ($.isFunction(item.callback)) {
                                    k.callbacks[key] = item.callback;
                                }
                            });
                            // FIXME: shouldn't this .html() be a .text()?
                            $('<span></span>').html(item._name || item.name || "").appendTo($t);
                            break;
                    }
                    
                    // disable key listener in <input>
                    if (item.type && item.type != 'sub' && item.type != 'html') {
                        $input
                            .on('focus', handle.focusInput)
                            .on('blur', handle.blurInput);
                        
                        if (item.events) {
                            $input.on(item.events, opt);
                        }
                    }
                
                    // add icons
                    if (item.icon) {
                        $t.addClass("icon icon-" + item.icon);
                    }
                }
                
                // cache contained elements
                item.$input = $input;
                item.$label = $label;

                // attach item to menu
                $t.appendTo(opt.$menu);
                
                // Disable text selection
                if (!opt.hasTypes && $.support.eventSelectstart) {
                    // browsers support user-select: none,
                    // IE has a special event for text-selection
                    // browsers supporting neither will not be preventing text-selection
                    $t.on('selectstart.disableTextSelect', handle.abortevent);
                }
            });
            // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
            if (!opt.$node) {
                opt.$menu.css('display', 'none').addClass('context-menu-root');
            }
            opt.$menu.appendTo(opt.appendTo || document.body);
        },
        resize: function($menu, nested) {
            // determine widths of submenus, as CSS won't grow them automatically
            // position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100;
            // kinda sucks hard...

            // determine width of absolutely positioned element
            $menu.css({position: 'absolute', display: 'block'});
            // don't apply yet, because that would break nested elements' widths
            // add a pixel to circumvent word-break issue in IE9 - #80
            $menu.data('width', Math.ceil($menu.width()) + 1);
            // reset styles so they allow nested elements to grow/shrink naturally
            $menu.css({
                position: 'static',
                minWidth: '0px',
                maxWidth: '100000px'
            });
            // identify width of nested menus
            $menu.find('> li > ul').each(function() {
                op.resize($(this), true);
            });
            // reset and apply changes in the end because nested
            // elements' widths wouldn't be calculatable otherwise
            if (!nested) {
                $menu.find('ul').addBack().css({
                    position: '',
                    display: '',
                    minWidth: '',
                    maxWidth: ''
                }).width(function() {
                    return $(this).data('width');
                });
            }
        },
        update: function(opt, root) {
            var $trigger = this;
            if (root === undefined) {
                root = opt;
                op.resize(opt.$menu);
            }
            // re-check disabled for each item
            opt.$menu.children().each(function(){
                var $item = $(this),
                    key = $item.data('contextMenuKey'),
                    item = opt.items[key],
                    disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true;

                // dis- / enable item
                $item[disabled ? 'addClass' : 'removeClass']('disabled');
                
                if (item.type) {
                    // dis- / enable input elements
                    $item.find('input, select, textarea').prop('disabled', disabled);
                    
                    // update input states
                    switch (item.type) {
                        case 'text':
                        case 'textarea':
                            item.$input.val(item.value || "");
                            break;
                            
                        case 'checkbox':
                        case 'radio':
                            item.$input.val(item.value || "").prop('checked', !!item.selected);
                            break;
                            
                        case 'select':
                            item.$input.val(item.selected || "");
                            break;
                    }
                }
                
                if (item.$menu) {
                    // update sub-menu
                    op.update.call($trigger, item, root);
                }
            });
        },
        layer: function(opt, zIndex) {
            // add transparent layer for click area
            // filter and background for Internet Explorer, Issue #23
            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>')
                .css({height: $win.height(), width: $win.width(), display: 'block'})
                .data('contextMenuRoot', opt)
                .insertBefore(this)
                .on('contextmenu', handle.abortevent)
                .on('mousedown', handle.layerClick);
            
            // IE6 doesn't know position:fixed;
            if (!$.support.fixedPosition) {
                $layer.css({
                    'position' : 'absolute',
                    'height' : $(document).height()
                });
            }
            
            return $layer;
        }
    };

// split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key
function splitAccesskey(val) {
    var t = val.split(/\s+/),
        keys = [];
        
    for (var i=0, k; k = t[i]; i++) {
        k = k[0].toUpperCase(); // first character only
        // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.
        // a map to look up already used access keys would be nice
        keys.push(k);
    }
    
    return keys;
}

// handle contextMenu triggers
$.fn.contextMenu = function(operation) {
    if (operation === undefined) {
        this.first().trigger('contextmenu');
    } else if (operation.x && operation.y) {
        this.first().trigger($.Event("contextmenu", {pageX: operation.x, pageY: operation.y}));
    } else if (operation === "hide") {
        var $menu = this.data('contextMenu').$menu;
        $menu && $menu.trigger('contextmenu:hide');
    } else if (operation === "destroy") {
        $.contextMenu("destroy", {context: this});
    } else if ($.isPlainObject(operation)) {
        operation.context = this;
        $.contextMenu("create", operation);
    } else if (operation) {
        this.removeClass('context-menu-disabled');
    } else if (!operation) {
        this.addClass('context-menu-disabled');
    }
    
    return this;
};

// manage contextMenu instances
$.contextMenu = function(operation, options) {
    if (typeof operation != 'string') {
        options = operation;
        operation = 'create';
    }
    
    if (typeof options == 'string') {
        options = {selector: options};
    } else if (options === undefined) {
        options = {};
    }
    
    // merge with default options
    var o = $.extend(true, {}, defaults, options || {});
    var $document = $(document);
    var $context = $document;
    var _hasContext = false;
    
    if (!o.context || !o.context.length) {
        o.context = document;
    } else {
        // you never know what they throw at you...
        $context = $(o.context).first();
        o.context = $context.get(0);
        _hasContext = o.context !== document;
    }
    
    switch (operation) {
        case 'create':
            // no selector no joy
            if (!o.selector) {
                throw new Error('No selector specified');
            }
            // make sure internal classes are not bound to
            if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {
                throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');
            }
            if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
                throw new Error('No Items specified');
            }
            counter ++;
            o.ns = '.contextMenu' + counter;
            if (!_hasContext) {
                namespaces[o.selector] = o.ns;
            }
            menus[o.ns] = o;
            
            // default to right click
            if (!o.trigger) {
                o.trigger = 'right';
            }
            
            if (!initialized) {
                // make sure item click is registered first
                $document
                    .on({
                        'contextmenu:hide.contextMenu': handle.hideMenu,
                        'prevcommand.contextMenu': handle.prevItem,
                        'nextcommand.contextMenu': handle.nextItem,
                        'contextmenu.contextMenu': handle.abortevent,
                        'mouseenter.contextMenu': handle.menuMouseenter,
                        'mouseleave.contextMenu': handle.menuMouseleave
                    }, '.context-menu-list')
                    .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)
                    .on({
                        'mouseup.contextMenu': handle.itemClick,
                        'contextmenu:focus.contextMenu': handle.focusItem,
                        'contextmenu:blur.contextMenu': handle.blurItem,
                        'contextmenu.contextMenu': handle.abortevent,
                        'mouseenter.contextMenu': handle.itemMouseenter,
                        'mouseleave.contextMenu': handle.itemMouseleave
                    }, '.context-menu-item');

                initialized = true;
            }
            
            // engage native contextmenu event
            $context
                .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);
            
            if (_hasContext) {
                // add remove hook, just in case
                $context.on('remove' + o.ns, function() {
                    $(this).contextMenu("destroy");
                });
            }
            
            switch (o.trigger) {
                case 'hover':
                        $context
                            .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
                            .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);
                    break;
                    
                case 'left':
                        $context.on('click' + o.ns, o.selector, o, handle.click);
                    break;
                /*
                default:
                    // http://www.quirksmode.org/dom/events/contextmenu.html
                    $document
                        .on('mousedown' + o.ns, o.selector, o, handle.mousedown)
                        .on('mouseup' + o.ns, o.selector, o, handle.mouseup);
                    break;
                */
            }
            
            // create menu
            if (!o.build) {
                op.create(o);
            }
            break;
        
        case 'destroy':
            var $visibleMenu;
            if (_hasContext) {
                // get proper options
                var context = o.context;
                $.each(menus, function(ns, o) {
                    if (o.context !== context) {
                        return true;
                    }
                    
                    $visibleMenu = $('.context-menu-list').filter(':visible');
                    if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) {
                        $visibleMenu.trigger('contextmenu:hide', {force: true});
                    }

                    try {
                        if (menus[o.ns].$menu) {
                            menus[o.ns].$menu.remove();
                        }

                        delete menus[o.ns];
                    } catch(e) {
                        menus[o.ns] = null;
                    }

                    $(o.context).off(o.ns);
                    
                    return true;
                });
            } else if (!o.selector) {
                $document.off('.contextMenu .contextMenuAutoHide');
                $.each(menus, function(ns, o) {
                    $(o.context).off(o.ns);
                });
                
                namespaces = {};
                menus = {};
                counter = 0;
                initialized = false;
                
                $('#context-menu-layer, .context-menu-list').remove();
            } else if (namespaces[o.selector]) {
                $visibleMenu = $('.context-menu-list').filter(':visible');
                if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) {
                    $visibleMenu.trigger('contextmenu:hide', {force: true});
                }
                
                try {
                    if (menus[namespaces[o.selector]].$menu) {
                        menus[namespaces[o.selector]].$menu.remove();
                    }
                    
                    delete menus[namespaces[o.selector]];
                } catch(e) {
                    menus[namespaces[o.selector]] = null;
                }
                
                $document.off(namespaces[o.selector]);
            }
            break;
        
        case 'html5':
            // if <command> or <menuitem> are not handled by the browser,
            // or options was a bool true,
            // initialize $.contextMenu for them
            if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) {
                $('menu[type="context"]').each(function() {
                    if (this.id) {
                        $.contextMenu({
                            selector: '[contextmenu=' + this.id +']',
                            items: $.contextMenu.fromMenu(this)
                        });
                    }
                }).css('display', 'none');
            }
            break;
        
        default:
            throw new Error('Unknown operation "' + operation + '"');
    }
    
    return this;
};

// import values into <input> commands
$.contextMenu.setInputValues = function(opt, data) {
    if (data === undefined) {
        data = {};
    }
    
    $.each(opt.inputs, function(key, item) {
        switch (item.type) {
            case 'text':
            case 'textarea':
                item.value = data[key] || "";
                break;

            case 'checkbox':
                item.selected = data[key] ? true : false;
                break;
                
            case 'radio':
                item.selected = (data[item.radio] || "") == item.value ? true : false;
                break;
            
            case 'select':
                item.selected = data[key] || "";
                break;
        }
    });
};

// export values from <input> commands
$.contextMenu.getInputValues = function(opt, data) {
    if (data === undefined) {
        data = {};
    }
    
    $.each(opt.inputs, function(key, item) {
        switch (item.type) {
            case 'text':
            case 'textarea':
            case 'select':
                data[key] = item.$input.val();
                break;

            case 'checkbox':
                data[key] = item.$input.prop('checked');
                break;
                
            case 'radio':
                if (item.$input.prop('checked')) {
                    data[item.radio] = item.value;
                }
                break;
        }
    });
    
    return data;
};

// find <label for="xyz">
function inputLabel(node) {
    return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;
}

// convert <menu> to items object
function menuChildren(items, $children, counter) {
    if (!counter) {
        counter = 0;
    }
    
    $children.each(function() {
        var $node = $(this),
            node = this,
            nodeName = this.nodeName.toLowerCase(),
            label,
            item;
        
        // extract <label><input>
        if (nodeName == 'label' && $node.find('input, textarea, select').length) {
            label = $node.text();
            $node = $node.children().first();
            node = $node.get(0);
            nodeName = node.nodeName.toLowerCase();
        }
        
        /*
         * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
         * Not being the sadistic kind, $.contextMenu only accepts:
         * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
         * Everything else will be imported as an html node, which is not interfaced with contextMenu.
         */
        
        // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
        switch (nodeName) {
            // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
            case 'menu':
                item = {name: $node.attr('label'), items: {}};
                counter = menuChildren(item.items, $node.children(), counter);
                break;
            
            // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
            case 'a':
            // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
            case 'button':
                item = {
                    name: $node.text(),
                    disabled: !!$node.attr('disabled'),
                    callback: (function(){ return function(){ $node.click(); }; })()
                };
                break;
            
            // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command

            case 'menuitem':
            case 'command':
                switch ($node.attr('type')) {
                    case undefined:
                    case 'command':
                    case 'menuitem':
                        item = {
                            name: $node.attr('label'),
                            disabled: !!$node.attr('disabled'),
                            callback: (function(){ return function(){ $node.click(); }; })()
                        };
                        break;
                        
                    case 'checkbox':
                        item = {
                            type: 'checkbox',
                            disabled: !!$node.attr('disabled'),
                            name: $node.attr('label'),
                            selected: !!$node.attr('checked')
                        };
                        break;
                        
                    case 'radio':
                        item = {
                            type: 'radio',
                            disabled: !!$node.attr('disabled'),
                            name: $node.attr('label'),
                            radio: $node.attr('radiogroup'),
                            value: $node.attr('id'),
                            selected: !!$node.attr('checked')
                        };
                        break;
                        
                    default:
                        item = undefined;
                }
                break;
 
            case 'hr':
                item = '-------';
                break;
                
            case 'input':
                switch ($node.attr('type')) {
                    case 'text':
                        item = {
                            type: 'text',
                            name: label || inputLabel(node),
                            disabled: !!$node.attr('disabled'),
                            value: $node.val()
                        };
                        break;
                        
                    case 'checkbox':
                        item = {
                            type: 'checkbox',
                            name: label || inputLabel(node),
                            disabled: !!$node.attr('disabled'),
                            selected: !!$node.attr('checked')
                        };
                        break;
                        
                    case 'radio':
                        item = {
                            type: 'radio',
                            name: label || inputLabel(node),
                            disabled: !!$node.attr('disabled'),
                            radio: !!$node.attr('name'),
                            value: $node.val(),
                            selected: !!$node.attr('checked')
                        };
                        break;
                    
                    default:
                        item = undefined;
                        break;
                }
                break;
                
            case 'select':
                item = {
                    type: 'select',
                    name: label || inputLabel(node),
                    disabled: !!$node.attr('disabled'),
                    selected: $node.val(),
                    options: {}
                };
                $node.children().each(function(){
                    item.options[this.value] = $(this).text();
                });
                break;
                
            case 'textarea':
                item = {
                    type: 'textarea',
                    name: label || inputLabel(node),
                    disabled: !!$node.attr('disabled'),
                    value: $node.val()
                };
                break;
            
            case 'label':
                break;
            
            default:
                item = {type: 'html', html: $node.clone(true)};
                break;
        }
        
        if (item) {
            counter++;
            items['key' + counter] = item;
        }
    });
    
    return counter;
}

// convert html5 menu
$.contextMenu.fromMenu = function(element) {
    var $this = $(element),
        items = {};
        
    menuChildren(items, $this.children());
    
    return items;
};

// make defaults accessible
$.contextMenu.defaults = defaults;
$.contextMenu.types = types;
// export internal functions - undocumented, for hacking only!
$.contextMenu.handle = handle;
$.contextMenu.op = op;
$.contextMenu.menus = menus;

})(jQuery);
/* Simple JavaScript Inheritance
 * By John Resig http://ejohn.org/
 * MIT Licensed.
 */
// Inspired by base2 and Prototype
(function(){
  var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
  // The base Class implementation (does nothing)
  this.Class = function(){};
  
  // Create a new Class that inherits from this class
  Class.extend = function(prop) {
    var _super = this.prototype;
    
    // Instantiate a base class (but only create the instance,
    // don't run the init constructor)
    initializing = true;
    var prototype = new this();
    initializing = false;
    
    // Copy the properties over onto the new prototype
    for (var name in prop) {
      // Check if we're overwriting an existing function
      prototype[name] = typeof prop[name] == "function" && 
        typeof _super[name] == "function" && fnTest.test(prop[name]) ?
        (function(name, fn){
          return function() {
            var tmp = this._super;
            
            // Add a new ._super() method that is the same method
            // but on the super-class
            this._super = _super[name];
            
            // The method only need to be bound temporarily, so we
            // remove it when we're done executing
            var ret = fn.apply(this, arguments);        
            this._super = tmp;
            
            return ret;
          };
        })(name, prop[name]) :
        prop[name];
    }
    
    // The dummy class constructor
    function Class() {
      // All construction is actually done in the init method
      if ( !initializing && this.init )
        this.init.apply(this, arguments);
    }
    
    // Populate our constructed prototype object
    Class.prototype = prototype;
    
    // Enforce the constructor to be what we expect
    Class.prototype.constructor = Class;

    // And make this class extendable
    Class.extend = arguments.callee;
    
    return Class;
  };
})();
if(!window.WG){WG = {}}

WG.ConsoleMessage = function(type, prefix, message){
	this.type = type
	this.prefix = prefix
	this.message = message
	
	this.div = $('<div class="message" />')
		.append(
			$('<span class="preflix" />')
				.addClass(type)
				.text(prefix + ": ")
		)
		.append(message)
	
}
	WG.ConsoleMessage.prototype.toString = function(){
		return this.prefix + ": " + this.message
	}

WG.Console = function(){
	this.pane = $('<div class="console"></div>')
	this.info("Console loaded...")
	$(document).ready(function(self){
			return function(){
				self.pane.prependTo($('#content'))
			}
		}(this)
	)
}
WG.Console.prototype.log = function(consoleMessage){
	this.pane.prepend(consoleMessage.div)
}
WG.Console.prototype.info = function(message){
	this.log(new WG.ConsoleMessage("info", "INFO", message))
}
WG.Console.prototype.warning = function(message){
	this.log(new WG.ConsoleMessage("warning", "WARNING", message))
}
WG.Console.prototype.error = function(message){
	this.log(new WG.ConsoleMessage("error", "ERROR", message))
}

WG.HiddenConsole = function(){
	this.messageLog = []
}
WG.HiddenConsole.prototype.log = function(consoleMessage){
	out = '...\n'
	if(consoleMessage.type == "error"){
		for(var i=Math.max(0, this.messageLog.length-5); i < this.messageLog.length;i++){
			message = this.messageLog[i]
			out += message.toString() + "\n"
		}
		out += consoleMessage.toString()
		alert("An error occurred: \n\n" + out)
	}
	this.messageLog.push(consoleMessage)
	if(window.console){
		console.log(consoleMessage.toString())
	}
}
WG.HiddenConsole.prototype.info = function(message){
	this.log(new WG.ConsoleMessage("info", "INFO", message))
}
WG.HiddenConsole.prototype.warning = function(message){
	this.log(new WG.ConsoleMessage("warning", "WARNING", message))
}
WG.HiddenConsole.prototype.error = function(message){
	this.log(new WG.ConsoleMessage("error", "ERROR", message))
}

if(!window.WG){WG = {}}

$.extend(WG, {
	NOTE_TEMPLATE: 'User:EpochFail/Snote'
})

$.extend(WG, {
	NOTE_TEMPLATE_RE: RegExp(
		'\\{\\{[ \\n\\r\\t]*' + 
		WG.NOTE_TEMPLATE.replace(/\:/g, "\\:").replace(/\//g, "\\/") + '[ \\n\\r\\t]*' + 
		'\\|?([^\\|]*?)(\\|.*)*\\}\\}'
	),
	TOKEN_MAP: {
		white_space:         ' +',
		number:              "[0-9]+(\\.[0-9]+)?|(\\.[0-9]+)",
		word:                '\\w+',
		entity:              '&\\w+;',
		list_item:           '\\n+(\\*|\\#|\\:)+',
		def_item:            '\\n+\\;',
		header:              '(\\n+|^)(=[^=].+?=|==[^=].+?==|===[^=].+?===|====[^=].+?====|=====[^=].+?=====)',
		open_table:          '\\n*\\{\\||<table[^>]*>',
		paragraph_break:     '\\n{2,}',
		open_note:           '\\{\\{[ \\n\\r\\t]*' + 
		                     WG.NOTE_TEMPLATE.replace(/\:/g, "\\:").replace(/\//g, "\\/") + 
		                     "[ \\n\\r\\t]*\\|?",
		open_image:          '\\[\\[(Image|File):',
		open_internal_link:  '\\[\\[',
		close_internal_link: '\\]\\]',
		external_link:       '\\[http[^\\]\\n]+\\]',
		open_template:       '\\{\\{',
		close_template:      '\\}\\}',
		comment:             '<!--(.|\\n|\\r)*?-->',
		ref:                 '<ref[^>]*/>|<ref[^>]*>(.|\\n|\\r)*?</ref>',
		math:                '<math[^>]*>(.|\\n|\\r)*?</math>',
		pre:                 '<pre[^>]*>(.|\\n|\\r)*?</pre>',
		source:              '<source[^>]*>(.|\\n|\\r)*?</source>',
		nowiki:              '<nowiki[^>]*>(.|\\n|\\r)*?</nowiki>',
		gallery:             '<gallery[^>]*>(.|\\n|\\r)*?</gallery>',
		close_table:         '\\|\\}|</table>',
		open_div:            '<div[^>]*>',
		close_div:           '</div>',
		table_row:           '\\|-',
		line_break:          '\\n',
		open_markup:         '<[^\\/][^>]*>',
		close_markup:        '<\\/[^>]*>',
		bold:                "'''",
		italics:             "''",
		quote:               "'|\\\"",
		ellipsis:            '\\.\\.\\.',
		period:              '\\.',
		comma:               ',',
		exlamation:          '!',
		question:            '\\?',
		colon:               '\\:',
		semicolon:           '\\;',
		bar:                 '\\|',
		other:               '.'
	},
	concatTokens: function(tokens, lower, upper){
		lower = lower||0
		upper = upper||tokens.length
		
		str = ''
		for(var i = lower; i < upper; i++){
			str += tokens[i].c
		}
		return str
	},
	NOTE_TEMPLATE: 'User:EpochFail/Snote'
})

/**
Converts MediaWiki markup into tokens.  For possible tokens and the regexp 
they match, see `WG.TOKEN_MAP`.  An arbitrary map of tokens can be provided.

:Parameters:
	markup : String
		mediawiki markup to be tokenized
	tokens : Object
		a map from "<token name>" to "<token regexp>" (as a string)
*/
WG.Tokenizer = Class.extend({
	init: function(markup, tokens){
		this.tokens = tokens || WG.TOKEN_MAP
		this.markup = markup
		var expressionParts = []
		for(type in this.tokens){
			expressionParts.push(this.tokens[type])
		}
		this.tokenRE = RegExp(expressionParts.join("|"), "gi")
		this.lookAhead = this.__nextToken()
	},

	/**
	Returns an array of all tokens found in `markup'.
	*/
	popAll: function(){
		var tokens = []
		while(this.peek()){
			tokens.push(this.pop())
		}
		return tokens
	},
	
	/**
	Returns the next token without removing it.
	*/
	peek: function(){
		if(this.lookAhead){
			return this.lookAhead
		}else{
			return null
		}
	},
	
	/**
	Returns and removes the next token.
	*/
	pop: function(){
		if(this.lookAhead){
			var temp = this.lookAhead
			var lastTime = new Date().getTime()/1000
			this.lookAhead = this.__nextToken()
			WG.WAIT_TIME += (new Date().getTime()/1000) - lastTime
			return temp
		}else{
			return null
		}
	},
	
	__nextToken: function(){
		var timeBefore = new Date().getTime() / 1000
		var match = this.tokenRE.exec(this.markup)
		WG.WAIT_TIME += (new Date().getTime() / 1000) - timeBefore
		if(!match){
			return null
		}else{
			var content = match[0]
			for(type in this.tokens){
				var re = RegExp("^" + this.tokens[type] + "$", "gi")
				if(re.test(content)){
					return {t:type, c:content}
				}
			}
			throw "Unexpected token content '" + content + "' matched no known token types."
		}
	}
})

/**
Converts MediaWiki markup into chunks of content.  Possible chunks to be 
returned include:
- header
- note
- template
- table
- list item
- div
- paragraph break
- line break
- sentence

 All chunks follow the scheme: 
     {t:"<type>", id:<chunkId>, c:"<content>"}

Joining the token content of chunks in order should reproduce the original 
MediaWiki markup.

:Parameters:
	markup : String
		mediawiki markup to be tokenized
	tokens : Object
		a map from "<token name>" to "<token regexp>" (as a string)
 */
WG.Chunker = function(markup, tokens){
	this.tokenizer = new WG.Tokenizer(markup, tokens)
	this.lookAhead = this.__nextChunk()
}
	
	/**
	Returns an array of all chunks found in `markup'.
	*/
	WG.Chunker.prototype.popAll = function(){
		var chunks = []
		while(this.peek()){
			chunks.push(this.pop())
		}
		return chunks
	}
	/**
	Returns the next chunk without removing it.
	*/
	WG.Chunker.prototype.peek = function(){
		if(this.lookAhead){
			return this.lookAhead
		}else{
			return null
		}
	}
	/**
	Returns and removes the next chunk
	*/
	WG.Chunker.prototype.pop = function(){
		if(this.lookAhead){
			var temp = this.lookAhead
			this.lookAhead = this.__nextChunk()
			return temp
		}else{
			return null
		}
	}
	WG.Chunker.prototype.__nextChunk = function(){
		if(!this.chunkId){
			this.chunkId   = 0
		}
		if(this.tokenizer.peek()){ //We have tokens to process
			switch(this.tokenizer.peek().t){
				case "open_template":
					return {
						t: "template", 
						c: WG.concatTokens(this.__template()), 
						id: this.chunkId++
					}
				case "open_note":
					var tokens = this.__note()
					return {
						t: "note", 
						c: WG.concatTokens(tokens), 
						id: this.chunkId++,
						val: WG.concatTokens(tokens, 1,tokens.length-1)
					}
				case "open_table":
					return {
						t: "table", 
						c: WG.concatTokens(this.__table()), 
						id: this.chunkId++
					}
				case "open_div":
					return {
						t: "div", 
						c: WG.concatTokens(this.__div()), 
						id: this.chunkId++
					}
				case "open_image":
					return {
						t: "image", 
						c: WG.concatTokens(this.__image()), 
						id: this.chunkId++
					}
				case "def_item":
					return {
						t: "definition", 
						c: WG.concatTokens(this.__definition()), 
						id: this.chunkId++
					}
				case "list_item":
					return {
						t: "def_list_item", 
						c: WG.concatTokens([this.tokenizer.pop()]),
						id: this.chunkId++
					}
				case "header":
					return {
						t: "header", 
						c: WG.concatTokens([this.tokenizer.pop()]), 
						id: this.chunkId++
					}
				case "paragraph_break":
					return {
						t: "paragraph_break", 
						c: WG.concatTokens([this.tokenizer.pop()]), 
						id: this.chunkId++
					}
				case "line_break":
					return {
						t: "break", 
						c: WG.concatTokens([this.tokenizer.pop()]),
						id: this.chunkId++
					}
				case "ref":
					return {
						t: "ref", 
						c: WG.concatTokens([this.tokenizer.pop()]), 
						id: this.chunkId++
					}
				/*case "math":
					return {
						t: "math", 
						c: WG.concatTokens([this.tokenizer.pop()]), 
						id: this.chunkId++
					}*/
				case "pre":
					return {
						t: "pre", 
						c: WG.concatTokens([this.tokenizer.pop()]), 
						id: this.chunkId++
					}
				case "gallery":
					return {
						t: "gallery", 
						c: WG.concatTokens([this.tokenizer.pop()]), 
						id: this.chunkId++
					}
				case "comment": 
					return {
						t: "comment",
						c: this.tokenizer.pop().c,
						id: this.chunkId++
					}
				case "white_space": 
					return {
						t: "white_space",
						c: this.tokenizer.pop().c,
						id: this.chunkId++
					}
				case "colon": 
					return {
						t: "colon",
						c: this.tokenizer.pop().c,
						id: this.chunkId++
					}
				case "source": 
					return {
						t: "source",
						c: this.tokenizer.pop().c,
						id: this.chunkId++
					}
				case "close_template":
				case "close_table":
				case "close_div":
				case "close_markup":
				case "exclamation":
				case "question":
				case "period":
				case "comma":
				case "table_row":
				case "close_internal_link":
				case "white_space":
				case "colon":
				case "semicolon":
				case "open_markup":
				case "italics":
				case "bold":
				case "quote":
				case "ellipsis":
				case "open_internal_link":
				case "external_link":
				case "entity":
				case "number":
				case "word":
				case "nowiki":
				case "math":
				case "other":
					tokens = this.__sentence()
					return {
						t: 'sentence', 
						c: WG.concatTokens(tokens), 
						id: this.chunkId++,
						tokens: tokens
					}
					break;
				default:
					throw "Unexpected token type '" + this.tokenizer.peek().t + "' found while generating chunks."
			}
		}else{ //No more tokens.  No more chunks. 
			return null
		}
	}
	WG.Chunker.prototype.__template = function(){
		var tokens = [this.tokenizer.pop()]
		var templates = 1
		while(this.tokenizer.peek() && templates > 0){
			switch(this.tokenizer.peek().t){
				case "open_template": //going one deeper!
					templates++
					break;
				case "close_template": //coming out of the templates
					templates--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	WG.Chunker.prototype.__div = function(){
		var tokens = [this.tokenizer.pop()]
		var divs = 1
		while(this.tokenizer.peek() && divs > 0){
			switch(this.tokenizer.peek().t){
				case "open_div": //going one deeper!
					divs++
					break;
				case "close_div": //coming out of the divs
					divs--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	WG.Chunker.prototype.__table = function(){
		var tokens = [this.tokenizer.pop()]
		var tables = 1
		while(this.tokenizer.peek() && tables > 0){
			switch(this.tokenizer.peek().t){
				case "open_table": //going one deeper!
					tables++
					break;
				case "close_table": //coming out of the tables
					tables--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	WG.Chunker.prototype.__definition = function(){
		var tokens = []
		var done = false
		while(this.tokenizer.peek() && !done){
			switch(this.tokenizer.peek().t){
				case "colon":           //---------------
					tokens.push(this.tokenizer.pop())
					tokens.push.apply(tokens, this.__extraDefinitionMatter())
					done = true;
					break;
				case "open_template":   //Suddently, a template.  Eat it and its children.
					tokens.push.apply(tokens, this.__template())
					break;
				case "open_internal_link": //Suddenly, a link.  Eat it and its children.
					tokens.push.apply(tokens, this.__internalLink())
					break;
				case "open_image": //Suddenly, an image.  Eat it and its children.
					tokens.push.apply(tokens, this.__image())
					break;
				case "line_break":
				case "definition_item":
				case "header":
				case "pre":
				case "gallery":
				case "source":
				case "open_note":       //----------------
				case "open_table":      //
				case "open_div":        //End of paragraph
				case "paragraph_break": //----------------
					done = true
					break;
				default:                //Everything else
					tokens.push(this.tokenizer.pop())
			}
		}
		return tokens
	}
	WG.Chunker.prototype.__extraDefinitionMatter = function(){
		var tokens = []
		var done = false
		while(this.tokenizer.peek() && !done){
			switch(this.tokenizer.peek().t){
				case "open_template":   //Suddently, a template.  Eat it and its children.
					tokens.push.apply(tokens, this.__template())
					break;
				case "open_internal_link": //Suddenly, a link.  Eat it and its children.
					tokens.push.apply(tokens, this.__internalLink())
					break;
				case "open_image": //Suddenly, an image.  Eat it and its children.
					tokens.push.apply(tokens, this.__image())
					break;
				case "line_break":
				case "def_item":
				case "header":
				case "pre":
				case "gallery":
				case "source":
				case "open_note":       //----------------
				case "open_table":      //
				case "open_div":        //End of paragraph
				case "paragraph_break": //----------------
					done = true
					break;
				default:                //Everything else
					tokens.push(this.tokenizer.pop())
			}
		}
		return tokens
	}
	WG.Chunker.prototype.__sentence = function(){
		var tokens = []
		var done = false
		while(this.tokenizer.peek() && !done){
			switch(this.tokenizer.peek().t){
				case "exclamation":     //---------------
				case "question":        //End of sentence
				case "period":          //
				case "colon":           //
				case "ellipsis":        //---------------
					tokens.push(this.tokenizer.pop())
					tokens.push.apply(tokens, this.__extraSentenceMatter())
					done = true;
					break;
				case "open_template":   //Suddently, a template.  Eat it and its children.
					tokens.push.apply(tokens, this.__template())
					break;
				case "open_internal_link": //Suddenly, a link.  Eat it and its children.
					tokens.push.apply(tokens, this.__internalLink())
					break;
				case "open_image": //Suddenly, an image.  Eat it and its children.
					tokens.push.apply(tokens, this.__image())
					break;
				case "def_item":
				case "header":
				case "pre":
				case "gallery":
				case "source":
				case "open_note":       //----------------
				case "open_table":      //
				case "open_div":        //End of paragraph
				case "paragraph_break": //----------------
					done = true
					break;
				default:                //Everything else
					tokens.push(this.tokenizer.pop())
			}
		}
		return tokens
	}
	WG.Chunker.prototype.__extraSentenceMatter = function(){
		var tokens = []
		var done = false
		while(this.tokenizer.peek() && !done){
			switch(this.tokenizer.peek().t){
				case "open_template":   //Suddently, a template.  Eat it and its children.
					tokens.push.apply(tokens, this.__template())
					break;
				case "open_internal_link": //----------------
				case "open_note":          //
				case "open_image":         // New element
				case "def_item":      //
				case "list_item":      //
				case "header":             //
				case "source":             //
				case "gallery":            //
				case "pre":                //----------------
				case "open_table":      //----------------
				case "open_div":        //New paragraph
				case "paragraph_break": //----------------
				case "open_markup":         //----------------
				case "italics":             //
				case "bold":                //
				case "open_internal_link":  // New sentence
				case "external_link":       //
				case "entity":              //
				case "word":                //
				case "other":               //----------------
					done = true
					break;
				default:                //Everything else
					tokens.push(this.tokenizer.pop())
			}
		}
		return tokens
	}
	WG.Chunker.prototype.__internalLink = function(){
		var tokens = [this.tokenizer.pop()]
		var links = 1
		while(this.tokenizer.peek() && links > 0){
			switch(this.tokenizer.peek().t){
				case "open_internal_link": //going one deeper!
					links++
					break;
				case "close_internal_link": //coming out of the links
					links--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	WG.Chunker.prototype.__image = function(){
		var tokens = [this.tokenizer.pop()]
		var links = 1
		while(this.tokenizer.peek() && links > 0){
			switch(this.tokenizer.peek().t){
				case "open_internal_link": //going one deeper!
					links++
					break;
				case "close_internal_link": //coming out of the links
					links--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	WG.Chunker.prototype.__note = function(){
		var tokens = [this.tokenizer.pop()]
		var templates = 1
		while(this.tokenizer.peek() && templates > 0){
			switch(this.tokenizer.peek().t){
				case "open_template": //going one deeper!
					templates++
					break;
				case "close_template": //coming out of the links
					templates--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	


if(!window.WG){WG = {}}

/**
A simple interface for interacting with a sentence in an article.

:Parameters:
	span : jQuery | DOM element
		the span containing the sentence to be edited

 */
WG.SentenceInteractor = Class.extend({
	init: function(span){
		this.span = $(span)
		if(this.span.hasClass("editing")){
			return
		}
		this.span.addClass("editing")
		var id = parseInt(this.span.attr("id").split("_")[1])
		this.chunk = WG.chunks.get(id)
		this.currentHTML = this.span.html()
		this.markup = {
			ltrim:   this.chunk.c.match(/^\s*/)[0],
			rtrim:   this.chunk.c.match(/\s*$/)[0],
			trimmed: $.trim(this.chunk.c)
		}
		
		this.div = $('<div />')
			.addClass("sentence_interactor")
			.insertAfter(this.span)
		
		this.pane = $('<div />')
			.addClass("pane")
			.css("position", "absolute")
			.appendTo(this.div)
			.hide()
		
		this.menu   = new WG.SentenceMenu(this)
		this.editor = new WG.SentenceEditor(this)
		this.editor.text(this.markup.trimmed)
		this.editor.summary(
			"Updating sentence starting with \"" + 
			this.markup.trimmed.substring(0, Math.min(50, this.markup.trimmed.length)) + 
			"...\""
		)
		this.show()
		this.resizer = function(interactor){return function(e){
			interactor.resize()
		}}(this)
		
		$(window).resize(this.resizer)
		
		//this.captureClickEvent = function(interactor){return function(e){
		//	if(e.button == 2 && e.ctrlKey){
		//		e.stopPropagation()
		//		return false;
		//	}
		//}}(this)
		//this.span.mousedown(this.captureClickEvent)
	},
	resize: function(){
		this.pane.css('width', $('#bodyContent .mw-content-ltr').innerWidth() - 15)
		this.div.css("height", this.pane.outerHeight())
	},
	preview: function(callback){
		WG.api.pages.preview(
			WG.PAGE_TITLE,
			this.markup.ltrim + 
			$.trim(this.editor.text()) + 
			this.markup.rtrim,
			function(interactor, callback){return function(html){
				html = html
					.replace(/<\/?p>/gi, '')
					.replace(/<br \/>\n<strong class="error">.*?<\/strong>/g, '')
				interactor.span.html(html)
				if(callback){callback(html)}
			}}(this, callback),
			function(interactor, callback){return function(error){
				WG.error(error)
			}}(this, callback)
		)
	},
	save: function(callback){
		this.editor.disable()
		
		//Update chunk
		this.chunk.c = this.markup.ltrim + 
			this.editor.text() + 
			this.markup.rtrim
		
		
		WG.chunks.save(
			this.menu.minor(),
			this.editor.summary() + WG.SUMMARY_SUFFIX,
			function(interactor, callback){return function(html){
				interactor.editor.enable()
				interactor.span.html(html)
				interactor.preview(
					function(interactor, callback){return function(html){
						interactor.currentHTML = html
						interactor.span.html(html)
						interactor.exit()
						if(callback){callback()}
					}}(interactor, callback)
				)
			}}(this, callback),
			function(interactor, callback){return function(error){
				WG.error(error)
				interactor.editor.enable()
			}}(this, callback)
		)
	},
	show: function(){
		this.pane.css('left', $('#bodyContent .mw-content-ltr').position().left - 15)
		this.pane.css('width', $('#bodyContent .mw-content-ltr').innerWidth() - 15)
		this.div.animate(
			{
				height: this.pane.outerHeight()
			},
			{
				duration: 200
			}
		)
		this.pane.slideDown(200)
	},
	hide: function(callback){
		this.pane.slideUp(200)
		this.div.animate(
			{height: 0}, 
			{
				duration: 200,
				complete: function(interactor, callback){return function(){
					interactor.div.hide()
					if(callback){callback()}
				}}(this, callback)
			}
		)
		
	},
	cancel: function(){
		this.span.html(this.currentHTML)
		this.exit()
	},
	exit: function(){
		this.hide(
			function(interactor){return function(){
				interactor.div.remove()
			}}(this)
		)
		this.span.removeClass("editing")
		$(window).unbind(this.resizer)
		//this.unbind('mousedown', this.captureClickEvent)
	}
})

WG.SentenceMenu = Class.extend({
	init: function(interactor){
		this.div = $('<div />')
			.addClass("menu")
			.prependTo(interactor.pane)
		
		
		this.cancel = $('<div />')
			.addClass("button")
			.addClass("cancel")
			.text("cancel")
			.attr("title", "cancel editing")
			.click(
				function(interactor){return function(){
					interactor.cancel()
				}}(interactor)
			)
			.appendTo(this.div)
		
		this.minorCheck = {
			div: $('<div />')
				.addClass("minor")
				.appendTo(this.div),
			label: $('<label />')
				.text("minor")
				.attr('for', "minor_sentence_edit"),
			checkbox: $('<input />')
				.attr('type', "checkbox")
				.attr('id', "minor_sentence_edit")
				.prop('checked', true)
		}
		this.minorCheck.div.append(this.minorCheck.label)
		this.minorCheck.div.append(this.minorCheck.checkbox)
		
		this.save = $('<div />')
			.addClass("button")
			.addClass("save")
			.addClass("primary")
			.text("save")
			.attr("title", "save your changes to the sentence")
			.click(
				function(interactor){return function(){
					interactor.save()
				}}(interactor)
			)
			.appendTo(this.div)
		
		this.preview = $('<div />')
			.addClass("button")
			.addClass("preview")
			.text("preview")
			.attr("title", "preview your change to the sentence")
			.click(
				function(interactor){return function(){
					interactor.preview()
				}}(interactor)
			)
			.appendTo(this.div)
	},
	minor: function(){
		return this.minorCheck.checkbox.is(":checked")
	},
	hide: function(){
		this.div.hide()
	},
	show: function(){
		this.div.show()
	}
})


WG.SentenceEditor = Class.extend({
	init: function(interactor){
		this.div = $('<div/>')
			.addClass('editor')
			.appendTo(interactor.pane)
		
		this.textPane = {
			textarea: $("<textarea />")
				.addClass("text")
				.appendTo(this.div)
		}
			
		this.summaryPane = {
			label: $('<label>')
				.text('Summary: ')
				.attr('for', interactor.chunk.id + "_summary")
				.appendTo(this.div),
			textarea: $("<textarea />")
				.addClass("summary")
				.appendTo(this.div)
				.attr('id', interactor.chunk.id + "_summary")
				.attr('rows', 1)
				
		}
	},
	
	text: function(val){
		if(val){
			this.textPane.textarea
				.attr("rows", Math.max(2, Math.ceil(val.length/80)))
				.val(val)
			
			return this
		}else{
			return this.textPane.textarea.val()
		}
	},
	
	summary: function(val){
		if(val){
			this.summaryPane.textarea.val(val)
			
			return this
		}else{
			return this.summaryPane.textarea.val()
		}
	},
	
	disable: function(){
		this.textPane.textarea.prop('disabled', true)
		this.summaryPane.textarea.prop('disabled', true)
	},
	
	enable: function(){
		this.textPane.textarea.prop('disabled', false)
		this.summaryPane.textarea.prop('disabled', false)
	}
})
if(!window.WG){WG = {}}

/**
Centers a jQuery element(s) horizontally in relation to another element (usually
a containing element).

:Parameters:
	of : DOM element | jQuery
		the element to center around

:Returns:
	this jQuery element
*/
jQuery.fn.center = function(of){
	of = $(of || window)
	this.css("position", "absolute")
	
	
	this.css(
		"left",
		(of.position().left + of.outerWidth()/2) - 
		(this.outerWidth()/2)
	)
	return this
}

/**
Gets the absolute bottom of an element including padding, borders and margin.

:Returns:
	int pixels of position
*/
jQuery.fn.outerBottom = function(){
	return this.position().top + this.outerHeight(true)
}

/**
Positions an element beneath another with a specified offset.

:Parameters:
	of : DOM element | jQuery
		the element to place beneath
	offset : int
		the number of pixels to offset the placement (defualts to zero)

:Returns:
	this jQuery element
*/
jQuery.fn.beneath = function(of, offset){
	offset = parseInt(offset || 0)
	of = $(of || $('body'))
	this.css("position", "absolute")
	this.css(
		"top",
		of.position().top + of.outerHeight(true) + offset
	)
	return this
}


WG.Gbutton = function(displayName){
	var innerSpan = $('<span />')
		.append($('<b />'))
		.append(
			$('<u />')
				.text(displayName)
		)
	
	return $('<button />')
		.attr("type", "button")
		.addClass("btn")
		.append($('<span />').append(innerSpan))

}

WG.lpad = function(number, width, padding){
	padding = padding || 0
	width -= number.toString().length;
	if ( width > 0 ){
		return new Array( width + (/\./.test( number ) ? 2 : 1) ).join( '0' ) + number;
	}
	return number;
}


WG.dumpObj = function(obj){
	str = 'Object: '
	for(thing in obj){
		str += "\n\t" + String(thing) + ": " + String(obj[thing])
	}
	WG.lastDumpedObj = obj
	return str
}

if(!window.WG){WG = {}}

$.extend(WG, {
	NOTE_HANDLE_HEIGHT: 25,
	NOTE_LINK_INIT:          "User:EpochFail/Note_link_init",
	NOTE_REFERENCE_INIT:     "User:EpochFail/Note_reference_init",
	NOTE_REFERENCE_TEMPLATE: "User:EpochFail/Note_reference"
})


/**
Represents a group of notes in a drawer.
*/
WG.NoteDrawerGroup = Class.extend({
	/**
	Constructs a new NoteDrawer note Group.
	*/
	init: function(offset, notes){
		this.div = $('<div class="group" />')
			.css("position", "absolute")
		this.notes = []
		notes = notes || []
		for(i in notes){note = notes[i]
			this.add(note)
		}
	},
	
	/**
	Adds a note to the group.
	*/
	add: function(note){
		//Add node in appropriate location in group
		for(var i in this.notes){var n = this.notes[i]
			if(n.offset() > note.offset()){
				this.notes.splice(i, 0, note)
				note.viewer.div.insertBefore(n.viewer.div)
				this.reposition()
				return
			}
		}
		//Otherwise, add to the end.
		this.notes.push(note)
		this.div.append(note.viewer.div)
		this.reposition()
	},
	
	/**
	Removes a note from the group if it exists in the group.  Otherwise does
	nothing.
	*/
	remove: function(note){
		for(i in this.notes){var n = this.notes[i]
			if(n == note){
				note.viewer.div.detach()
				this.notes.splice(i, 1)
				this.reposition()
			}
		}
	},
	
	/**
	The absolute position top of the drawer including margin, padding and 
	border.
	*/
	top: function(){
		return this.div.position().top
	},
	
	/**
	The absolute position bottom of the drawer including margin, padding and
	border.
	*/
	bottom: function(){
		//return this.div.outerBottom()
		
		//instead, return what the bottom *should* be
		return this.top() + (WG.NOTE_HANDLE_HEIGHT * this.notes.length)
	},
	
	/**
	Detaches notes and removed self from DOM. 
	*/
	del: function(){
		this.div.children().detach()
		this.div.remove()
	},
	
	/**
	
	*/
	reposition: function(){
		if(this.notes.length > 0){
			this.div.css("top", this.notes[0].offset())
		}
	}
})

/**
Positions and aligns notes on the right side of the screen in an intelligent way.
*/
WG.NoteDrawer = Class.extend({
	/**
	Constructs a new NoteDrawer
	*/
	init: function(){
		this.div = $('<div class="note_drawer" />')
			.appendTo('#bodyContent')
			.css('position', 'absolute')
			.css('top', 0)
			.css(
				'right', 
				-15
			)
			.css('height', $('#bodyContent').height())
		
		this.groups = []
		
		$(window).resize(
			function(drawer){return function(e){
				if(drawer.reloadTimer){
					clearTimeout(
						drawer.resizeTimer
					)
				}
				
				drawer.reloadTimer = setTimeout(
					function(drawer){return function(e){
						drawer.reload()
					}}(drawer)
				)
			}}(this)
		)
	},
	
	/**
	Redraws the note drawer and re-arranges the notes when necessary.  For 
	example, when the window is resized.
	*/
	reload: function(){
		this.clear()
		var oldGroups = this.groups
		this.groups = []
		for(var i in oldGroups){var group = oldGroups[i]
			for(i in group.notes){var note = group.notes[i]
				this.add(note)
			}
		}
	},
	
	/**
	Adds a set of notes to the drawer.
	*/
	load: function(notes){
		this.clear()
		this.groups = []
		for(var i in notes){note = notes[i]
			//make sure the note is hidden so we can align it correctly.
			note.viewer.hide()
			
			//get the location that this note would want its handle positioned.
			
			//If this is the first note or there is no overlap
			if(
				this.groups.length == 0 ||
				this.groups[this.groups.length-1].bottom() < note.offset()
			){
				//Easy case.  We just create a new group at our desired offset
				var group = new WG.NoteDrawerGroup(note.offset())
				group.add(note)
				
				//Add it to the drawer
				this.div.append(group.div)
				
				//Add it to our list of groups
				this.groups.push(group)
			}else{
				//There is currently a group in the way of where
				//we want to put this note's handle
				//Let's just add it to the previous group.
				this.groups[this.groups.length-1].add(note)
			}
		}
		$('div.note_viewer').css('overflow', 'visible')
	},
	
	/**
	Adds a note to the drawer in the most appropriate group
	*/
	add: function(note){
		for(var i in this.groups){var group = this.groups[i]
			if(group.bottom() < note.offset()){
				//Not to the offset yet.
			}else if(group.top() <= note.offset() + WG.NOTE_HANDLE_HEIGHT){
				//We want to be inside of a group that already exists
				group.add(note)
				return
			}else{
				//We passed up the spot we want to put this note.
				//Drop it in it's desired spot.
				var group = new WG.NoteDrawerGroup(note.offset())
				group.add(note)
				this.div.append(group.div)
				
				//Insert in position.  Don't put it *before* the
				//beginning.
				this.groups.splice(Math.max(0, i-1), 0, group)
				return
			}
		}
		//If you get here, that means we are adding a note that is below
		//all previous notes.  We can just add it where we want it. 
		var group = new WG.NoteDrawerGroup(note.offset())
		group.add(note)
		this.div.append(group.div)
		this.groups.push(group)
	},
	
	/**
	Removes a note from the drawer.
	*/
	remove: function(note){
		note.viewer.div.detach()
		for(var i in this.groups){var group = this.groups[i]
			//We don't have to check if the note is in the group
			//since this function does nothing if it isn't.
			group.remove(note)
		}
	},
	
	/**
	Clears all groups from the drawer.
	*/
	clear: function(){
		if(this.groups){
			for(var i in this.groups){var group = this.groups[i]
				group.del()
			}
		}
	}
})

/**
Represents a note in an article.
*/
WG.Note = Class.extend({
	init: function(span){
		this.span  = $(span)
		if(!this.span.attr('id')){
			throw "Span missing id."
		}
		this.id = this.span.attr('id')
		this.viewer = new WG.NoteViewer(this)
		this.editor = new WG.NoteEditor(this)
		this.viewer.div.append(this.editor.div)
		this.hide()
		
		this.span
			.click(
				function(note){return function(e){
					note.toggle()
				}}(this)
			)
			.hover(
				function(note){return function(e){
					note.viewer.handle.div.addClass("hover")
				}}(this),
				function(note){return function(e){
					note.viewer.handle.div.removeClass("hover")
				}}(this)
			)
	},
	chunkId: function(){
		return this.chunk.id
	},
	pageTitle: function(){
		return WG.TALK_PAGE_TITLE + "/" + this.id
	},
	offset: function(){
		return this.span.position().top + this.span.outerHeight(true)+3//fudge
	},
	cancel: function(){
		this.editor.compress()
	},
	
	
	/**
	Toggles between showing and hiding the note viewer/editor
	*/
	toggle: function(){
		if(this.hidden){
			this.show()
		}else{
			this.hide()
		}
	},
	
	/**
	Animated hide operation
	*/
	hide: function(callback){
		this.hidden = true
		this.editor.hide()
		//this.viewer.div.css("overflow", "hidden")
		this.viewer.hide(callback)
	},
	
	/**
	Animated show operation.
	*/
	show: function(callback){
		this.hidden = false
		this.viewer.show(
			function(note, callback){return function(){
				note.editor.show()
				if(callback){callback()}
			}}(this, callback)
		)
		
	},
	
	/**
	Loads a preview of whatever if in the editor into the viewer using the API
	*/
	preview: function(callback){
		WG.api.pages.preview(
			WG.TALK_PAGE_TITLE,
			this.editor.val(),
			function(note, callback){return function(html){
				note.viewer.view(html)
				if(callback){callback(html)}
			}}(this, callback),
			function(error){
				WG.console.error(error)
			}
		)
	},
	
	/**
	Saves a new version of the note based on what is currently in the editor
	*/
	save: function(callback){
		//First disable the editor.  No clicking or typing while I'm saving!
		this.editor.disable()
		
		//Start the call to save the current note.
		WG.api.pages.save(
			this.pageTitle(),
			this.token,
			this.preamble + this.editor.val(),
			"Updating note",
			false,
			function(note, callback){return function(){
				note.preview(
					function(note, callback){return function(html){
						note.savedHTML = html
						if(callback){callback()}
					}}(this, callback)
				)
				//Now you can use the editor again
				note.editor.enable()
				note.hide()
			}}(this, callback),
			function(note, callback){return function(message){
				WG.console.error(message)
				
				//Something bad happened, but you're welcome to try again.
				note.editor.enable()
			}}(this, callback)
		)
	},
	
	/**
	Removes a note placeholder from the chunks.
	*/
	remove: function(callback){
		if(confirm("Are you sure you'd like to remove this note?")){
			this.editor.disable()
			
			var summary = 'Removing note with "'
			summary += this.editor.val().substring(0, 250-(summary.length+4)) + '..."'
			
			WG.chunks.remove(this.chunk)
			WG.chunks.save(
				false,
				summary,
				function(note, callback){return function(){
					note.span.remove()
					//Remove self from drawer
					note.hide(
						function(note){return function(){
							WG.noteDrawer.remove(note)
						}}(note)
					)
				}}(this, callback),
				function(note, callback){return function(error){
					WG.console.error(error)
					this.editor.enable()
				}}(this, callback)
			)
		}
	},
	
	/**
	Refresh with subpage content
	**/
	load: function(){
		WG.api.pages.get(
			this.pageTitle(),
			function(note){return function(markup, page){
				note.__loadMarkup(markup)
				note.preview()
				note.editor.enable()
				note.token = page.edittoken
			}}(this),
			function(note){return function(message){
				WG.console.error("Could not load note markup " + note.id + ": " + message)
			}}(this)
		)
	},
	__loadMarkup: function(markup){
		var noteHeaderRE = RegExp(
			"{{\\s*" + 
			WG.NOTE_REFERENCE_TEMPLATE
				.replace("/", "\\/")
				.replace("_", "(_| )") + 
			"\\s*\\|(\\s|.)*?}}\n*"
		)
		var match = markup.match(noteHeaderRE)
		if(match){
			//Found a note header template.  Yay!
			var parts = markup.split(match[0])
			this.preamble = parts[0] + match[0],
			this.editor.val(parts.slice(1).join(match[0]))
		}else{
			//No template :(.  Try to process the first level two header.
			var level2RE = /(^|\n)+==[^\n]+?==/
			match = markup.match(level2RE)
			if(match){
				var parts = markup.split(match[0])
				this.preamble = parts[0] + match[0],
				this.editor.val(parts.slice(1).join(match[0]))
					
			}else{
				this.preamble = '{{' + WG.NOTE_HEADER_TEMPLATE + "|" + this.id + "}}",
				this.editor.val(markup)
			}
		}
	}
})

WG.OldNote = WG.Note.extend({
	init: function(chunk, span){
		this._super(span)
		//var id = parseInt(chunkSpan.attr('id').split("_")[1])
		if(chunk.t != "note"){
			throw "Non-note chunk(" + chunk.id + ") type '" + chunk.t + "' to be edited.  No can do duder."
		}
		this.chunk = chunk
		this.load()
	}
})

WG.NewNote = WG.Note.extend({
	init: function(previousId, noteClass, id){
		//Creates a human-readable, searchable timestamp
		var id = [
				[
					WG.lpad(d.getUTCFullYear(), 4), 
					WG.lpad(d.getUTCMonth()+1), 
					WG.lpad(d.getUTCDate(), 2)
				].join('-'),
				[
					WG.lpad(d.getUTCHours(), 2), 
					WG.lpad(d.getUTCMinutes(), 2), 
					WG.lpad(d.getUTCSeconds(), 2)
				].join(":")
			].join('_')
		
		var span = $('<span />')
			.addClass(noteClass)
			.css("display", "inline-block;")
			.attr("id", id)
		
		this._super(span)
		this.previousChunk = WG.chunks.get(previousId)
		this.show(function(note){return function(){note.editor.expand()}}(this))
	},
	saved: function(){
		return this.chunk != undefined
	},
	chunkId: function(){
		if(this.saved()) return this._super()
		
		return undefined
	},
	save: function(callback){
		if(this.saved()) return this._super(callback)
		else{this.__createPage(callback)}
	},
	__createPage: function(callback){
		
		//First disable the editor.  No clicking or typing while I'm saving!
		this.editor.disable()
		
		
		//Create the note page with new note content
		WG.api.pages.create(
			this.pageTitle(),
			"== Re. Inline [[{{subst:" + WG.NOTE_LINK_INIT + "}}|note]]==\n" +
			"{{subst:" + WG.NOTE_REFERENCE_INIT + "}}\n" + 
			this.editor.val(),
			"Creating note page for [[" + WG.PAGE_TITLE + "]]",
			function(note, callback){return function(save){
				//Created the note page.  Now update the article with the placeholder
				note.__insertIntoArticle(callback)
				note.__appendToTalkPage()
			}}(this, callback),
			function(note){return function(message){
				WG.console.error("Could not create note subpage: " + message)
				note.editor.enable()
			}}(this)
		)
	},
	__insertIntoArticle: function(callback){
		
		//Update chunks
		this.chunk = {
			id: this.previousChunk.id+1,
			t: "note",
			c: "{{" + WG.NOTE_TEMPLATE + "|" + this.id + "}}",
			val: this.id
		}
		WG.chunks.insert(this.chunk)
		
		//Save a new version
		WG.chunks.save(
			false,
			"Inserting note placeholder for [[" + this.pageTitle() + "]]",
			function(note, callback){return function(save){
				var summary = 'Inserting [[' + note.pageTitle() + '|note]] with "'
				summary += note.editor.val().substring(0, 250-(summary.length+4)) + '..."'
				
				//Preview the new content
				note.preview(
					function(note, callback){return function(html){
						note.savedHTML = html
						if(callback){callback()}
					}}(this, callback)
				)
				
				//Re-enable the editor
				note.editor.enable()
				note.hide()
			}}(this, callback),
			function(note, callback){return function(message){
				WG.console.error("Could not update article with note placeholder: " + message)
				note.editor.enable()
			}}(this, callback)
		)
	},
	__appendToTalkPage: function(){
		WG.api.pages.append(
			WG.TALK_PAGE_TITLE,
			"\n\n{{" + this.pageTitle() + "}}",
			"Adding section for new [[" + this.pageTitle() + "|note]]",
			function(save){},
			function(message){
				WG.console.error("Could not append note to talk page: " + message)
			}
		)
	},
	cancel: function(){
		if(this.saved()) return this._super()
		
		//DESTROY EVERYTHING and forget we even started
		this.span.remove()
		
		//Remove self from drawer
		this.hide(
			function(note){return function(){
				WG.noteDrawer.remove(note)
			}}(this)
		)
	}
})



/**
An animated interface for viewing a note embedded in wiki markup.
*/
WG.NoteViewer = Class.extend({
	init: function(note){
		this.div = $('<div class="note_viewer" />')
		
		this.handle = {
			div: $('<div class="handle" />')
				.appendTo(this.div)
				.click(
					function(note){return function(e){
						note.toggle()
					}}(note)
				)
				.hover(
					function(note){return function(e){
						note.span.addClass("hover")
					}}(note),
					function(note){return function(e){
						note.span.removeClass("hover")
					}}(note)
				)
		}
		var paneDiv = $('<div class="pane" />')
		this.pane = {
			div: paneDiv
				.hide()
				.appendTo(this.div),
			view: $('<div class="view" />')
				.appendTo(paneDiv)
		}
		
		this.hidden = true
	},
	
	/**
	Loads new HTML into the note viewer.
	*/
	view: function(html){
		this.pane.view.html(html)
	},
	
	/**
	Animated hide operation
	*/
	hide: function(callback){
		//this.viewer.div.css("overflow", "hidden")
		this.pane.div.slideUp(
			200,
			function(viewer, callback){return function(){
				viewer.div.animate(
					{
						width: 0, 
						right: 0
					},
					{
						complete: function(callback){return function(){
							if(callback){callback()}
						}}(callback)
					}
				)
			}}(this, callback)
		)
	},
	
	/**
	Animated show operation.
	*/
	show: function(callback){
		this.div.animate(
			{width: 500, right: 500},
			{
				complete: function(viewer){return function(){
					viewer.pane.div.slideDown(200)
					if(callback){callback()}
				}}(this)
			}
		)
	}
})

/**
An editor for creating and updating notes.
*/
WG.NoteEditor = Class.extend({
	init: function(note){
		this.note = note
		
		this.div = $('<div class="editor" />')
		
		this.remover = {
			div: $('<div class="button remove"/>')
				.text("remove")
				.appendTo(this.div)
				.click(
					function(editor){return function(e){
						editor.note.remove()
					}}(this)
				)
		}
		
		this.opener = {
			div: $('<div class="button open"/>')
				.text("edit")
				.addClass("primary")
				.appendTo(this.div)
				.click(
					function(editor){return function(e){
						editor.expand()
					}}(this)
				)
		}
		
		this.textarea =  $('<textarea />')
			.hide()
			.appendTo(this.div)
		
		
		this.canceller = {
			div: $('<div class="button cancel"/>')
				.text("cancel")
				.hide()
				.appendTo(this.div)
				.click(
					function(editor){return function(e){
						if(editor.div.hasClass("disabled")){return}
						editor.note.cancel()
					}}(this)
				)
		}
		
		this.saver = {
			div: $('<div class="button save"/>')
				.text("save")
				.addClass("primary")
				.hide()
				.appendTo(this.div)
				.click(
					function(editor){return function(e){
						if(editor.div.hasClass("disabled")){return}
						editor.note.save()	
					}}(this)
				)
		}
		
		this.previewer = {
			div: $('<div class="button preview"/>')
				.text("preview")
				.hide()
				.appendTo(this.div)
				.click(
					function(editor){return function(e){
						if(editor.div.hasClass("disabled")){return}
						editor.note.preview()
					}}(this)
				)
		}
		this.div.append($('<div style="clear:both;"/>').css('height', 0))
			
	},
	
	/**
	Get and sets the value of the editor (what's in the text area)
	*/
	val: function(val){
		if(val){
			//Setting the value
			this.textarea
				.val(val)
				.attr(
					"rows", 
					Math.max(6, Math.floor(val.length/30))
				)
		}else{
			//Asking for the value
			return this.textarea.val()
		}
	},
	
	hide: function(){
		this.div.hide()
	},
	show: function(){
		this.div.show()
	},
	
	/**
	Hides the edit pane with a nice little animation
	*/
	compress: function(){
		this.textarea.slideUp(
			200,
			function(editor){return function(e){
				//Show the other buttons
				editor.saver.div.hide()
				editor.previewer.div.hide()
				editor.canceller.div.hide()
				
				//Hide the edit div
				editor.opener.div.show()
				editor.remover.div.show()
			}}(this)
		)
		
	},
	
	/**
	Shows the edit pane with a nice little animation
	*/
	expand: function(){
		this.div.show()
		//Hide the edit div
		this.opener.div.hide()
		this.remover.div.hide()
		
		//Show the other buttons
		this.saver.div.show()
		this.previewer.div.show()
		this.canceller.div.show()
		
		//Expand the text area
		this.textarea.slideDown(200)
		this.textarea.focus()
	},
	
	/**
	Cancels the editing operation.  Reverts the markup in tghe textarea back
	to the original and closes the editor.
	*/
	cancel: function(){
		
		//hide
		this.compress()
		
		//restore old markup into textarea
		this.textarea.val(this.note.chunk.val)
		
		//Revert the viewer
		this.note.viewer.revert()
	},
	
	/**
	Disables the buttons and text area in the editor.  This is useful when
	new input should be restricted while an operation is being performed.
	*/
	disable: function(){
		//Disable text area
		this.textarea.prop('disabled', true)
		
		//add disabled class
		this.div.addClass("disabled")
	},
	
	/**
	Enables (or re-enables) the buttons and text editor. 
	*/
	enable: function(){
		//Re-enable textarea
		this.textarea.prop('disabled', false)
		
		//Remove the disabled class
		this.div.removeClass("disabled")
	},
	
	/**
	Focuses the cursor in the textarea
	*/
	focus: function(){
		this.textarea.focus()
	}
})
if(!window.WG){WG = {}}


WG.API = Class.extend({
	init: function(){
		this.url = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + "/api.php"
		this.pages = new WG.Pages(this)
	}
})

WG.Pages = Class.extend({
	init: function(api){
		this.api = api
	},
	get: function(title, success, error){
		success = success || function(){}
		error   = error   || function(){}
		
		WG.console.info("API: Requesting the current version of " + title + "...")
		$.ajax({
			url: this.api.url,
			dataType: "json",
			data: {
				action: 'query',
				prop:   'revisions|info',
				titles: title,
				rvprop: 'content|timestamp',
				intoken:'edit',
				format: 'json'
			},
			type: "POST",
			context: this,
			success: function(success, error){return function(data, status){
				//alert(WG.dumpObj(this))
				if(status != "success"){
					error("The API is unavilable: " + status)
				}else if(data.error){
					error("Received an error from the API: " + data.error.code + " - " + data.error.info)
				}else if(!data.query || !data.query.pages){
					error("Received an unexpected response from the API: " + WG.dumpObj(data))
				}else {
					for(key in data.query.pages){
						var page = data.query.pages[key]
					}
					if(page.revisions){
						var markup = page.revisions[0]['*']
						WG.console.info("API: Received revision " + page.lastrevid + " of " + page.title + " with markup of length " + markup.length)
					}else{
						var markup = undefined
						WG.console.info("API: Received info for missing page " + page.title)
					}
					
					success(markup, page)
				}
			}}(success, error),
			error: function(error){return function(jqXHR, status, message){
				//Sometimes an error happens when the request is 
				//interrupted by the user changing pages. 
				if(status != 'error' || message != ''){
					error("An error occurred while contacting Wikipedia's API: " + status + ": " + message)
				}
			}}(error)
		})
	},
	append: function(title, markup, summary, success, error){
		success = success || function(){}
		error   = error   || function(){}
		
		WG.console.info("API: Trying to append to " + title + "...")
		this.get(
			title,
			function(api, title, markup, summary, success, error){return function(___, page){
				WG.console.info("API: Appending markup of length " + markup.length + " to " + title + "...")
				$.ajax({
					url: api.url,
					dataType: "json",
					data: {
						action:     'edit',
						title:      title,
						appendtext: markup,
						token:      page.edittoken,
						summary:    summary,
						format:     'json'
					},
					type: "POST",
					success: function(summary, success, error){return function(data, status){
						if(status != "success"){
							error("The API is unavilable: " + status)
						}else if(data.error){
							error("Received an error from the API: " + data.error.code + " - " + data.error.info)
						}else if(!data.edit || !data.edit.result){
							error("Received an unexpected response from the API: " + WG.dumpObj(data))
						}else if(data.edit.result != "Success"){
							error("Saving the edit failed: " + WG.dumpObj(data.edit))
						}else{
							WG.console.info("API: Successfully appended text in revision " + data.edit.newrevid + " of " + data.edit.title + ": " + summary)
							success(data.edit)
						}
					}}(summary, success, error),
					error: function(error){return function(jqXHR, status, message){
						//Sometimes an error happens when the request is 
						//interrupted by the user changing pages. 
						if(status != 'error' || message != ''){
							error("An error occurred while contacting Wikipedia's API: " + status + ": " + message)
						}
					}}(error)
				})
			}}(this.api, title, markup, summary, success, error),
			function(error){return function(message){
				error(message)
			}}(error)
		)
	},
	save: function(title, token, touched, markup, summary, minor, success, error){
		success = success || function(){}
		error   = error   || function(){}
		
		WG.console.info("API: Saving a new revision of " + title + " with mark of length " + markup.length + "...")
		$.ajax({
			url: this.api.url,
			dataType: "json",
			data: {
				action:          'edit',
				title:           title,
				text:            markup,
				token:           token,
				basetimestamp:   touched,
				summary:         summary,
				minor:           minor,
				format:          'json'
			},
			type: "POST",
			success: function(summary, success, error){return function(data, status){
				if(status != "success"){
					error("The API is unavilable: " + status)
				}else if(data.error){
					error("Received an error from the API: " + data.error.code + " - " + data.error.info)
				}else if(!data.edit || !data.edit.result){
					error("Received an unexpected response from the API: " + WG.dumpObj(data))
				}else if(data.edit.result != "Success"){
					error("Saving the edit failed: " + WG.dumpObj(data.edit))
				}else{
					WG.console.info("API: Successfully saved revision " + data.edit.newrevid + " of " + data.edit.title + ": " + summary)
					success(data.edit)
				}
			}}(summary, success, error),
			error: function(error){return function(jqXHR, status, message){
				//Sometimes an error happens when the request is 
				//interrupted by the user changing pages. 
				if(status != 'error' || message != ''){
					error("An error occurred while contacting Wikipedia's API: " + status + ": " + message)
				}
			}}(error)
		})
	},
	create: function(title, markup, summary, success, error){
		success = success || function(){}
		error   = error   || function(){}
		
		WG.console.info("API: Trying to create " + title + "...")
		this.get(
			title,
			function(api, title, markup, summary, success, error){return function(___, page){
				if(page.missing == undefined){
					throw "Failed to create page " + title + ".  Already exists."
				}
				api.save(title, page.edittoken, markup, summary, false, success, error)
			}}(this, title, markup, summary, success, error),
			function(error){return function(message){
				error(message)
			}}(error)
		)
	},
	preview: function(title, markup, success, error){
		success = success || function(){}
		error   = error   || function(){}
		
		WG.console.info("API: Sending markup of length " + markup.length + " for " + title + " to be parsed...")
		$.ajax({
			url: this.api.url,
			dataType: "json",
			data: {
				action: 'parse',
				title:  title,
				text:   markup,
				prop:   'text',
				pst:    true,
				format: 'json'
			},
			type: "POST",
			context: this,
			success: function(success, failure){return function(data, status){
				if(status != "success"){
					error("The API is unavilable: " + status)
				}else if(data.error){
					error("Received an error from the API: " + data.error.code + " - " + data.error.info)
				}else if(!data.parse || !data.parse.text || !data.parse.text['*']){
					error("Received an unexpected response from the API: " + WG.dumpObj(data))
				}else{
					var html = data.parse.text['*']
						.replace(/<!--(.|\n|\r)*?-->/gi, '')
					success(html)
				}
			}}(success, error),
			error: function(error){return function(jqXHR, status, message){
				//Sometimes an error happens when the request is 
				//interrupted by the user changing pages. 
				if(status != 'error' || message != ''){
					error("An error occurred while contacting Wikipedia's API: " + status + ": " + message)
				}
			}}(error)
		})
		
	}
})
if(!window.WG){WG = {}}

WG.Chunks = Class.extend({
	init: function(chunks){
		this.chunks = chunks
	},
	get: function(id, type){
		id = parseInt(id)
		if(!this.chunks[id]){
			throw "Chunk id " + id + " was not found."
		}else if(type && this.chunks[id].t != type){
			throw "Chunk id " + id + " is of type '" + this.chunks[id].t + "', not '" + type + "'"
		}else{
			return this.chunks[id]
		}
	},
	insert: function(chunk){
		this.chunks.splice(chunk.id, 0, chunk)
		
		//Update future chunks and representation
		for(var i=this.chunks.length-1;i>=chunk.id+1;i--){
			var upChunk = this.chunks[i]
			var span = $('#chunk_' + upChunk.id)
			span.attr('id', 'chunk_' + i)
			upChunk.id = i
		}
	},
	remove: function(chunk){
		//Remove chunk from chunk list
		this.chunks.splice(chunk.id, 1)
		
		//Remove id from span
		$('#chunk_' + chunk.id).removeAttr('id')
		
		//Update the affected spans and chunks
		for(var i=chunk.id;i<this.chunks.length;i++){
			var upChunk = this.chunks[i]
			var span = $('#chunk_' + upChunk.id)
			
			span.attr('id', 'chunk_' + i)
			
			upChunk.id = i
		}
	},
	toString: function(){
		var newMarkup = ''
		for(i in this.chunks){
			newMarkup += this.chunks[i].c
		}
		return newMarkup
	},
	remarkup: function(sentenceClass, noteClass, headerClass){
		markup = ''
		for(var i in this.chunks){
			var chunk = this.chunks[i]
			if(chunk.t == "sentence" || chunk.t == "definition"){
				markup += (
					'<span class="' + sentenceClass + 
					'" id="chunk_' + i + '">' + 
					chunk.c + '</span>'
				)
			}else if(chunk.t == "note"){
				markup += (
					'<span class="' + noteClass + 
					'" id="chunk_' + i + '">' + 
					chunk.c + '</span>'
				)
			}/*else if(chunk.t == "header"){
				markup += (
					'<div class="' + headerClass + 
					'" id="chunk_' + i + '">' + 
					chunk.c + '\n</div>'
				)
			}*/else{
				markup += chunk.c
			}
		}
		return markup
	},
	save: function(minor, summary, success, error){
		WG.api.pages.save(
			WG.PAGE_TITLE,
			WG.token,
			WG.touched,
			this.toString(),
			summary,
			minor,
			success,
			error
		)
	}
})
if(!window.WG){WG = {}}
$.extend(WG, {
	SENTENCE_CLASS: "WG_sentence",
	NOTE_CLASS: "WG_snote",
	HEADER_CLASS: "WG_header",
	MARKUP_CLASS: "WG_markup",
	CONTEXT_MENU: $('<ul style="display:none" class="contextMenu"/>')
		.append($('<li />')
			.addClass('edit')
			.append($('<a />')
				.attr('href', '#edit')
				.text('Edit sentence')
			)
		)
		.append($('<li />')
			.addClass('new_note')
			.append($('<a />')
				.attr('href', '#new_note')
				.text('Insert note')
			)
		)
		.appendTo($('body')),
	SUB_NOTE_CLASS: "note_container",
	WAIT_TIME: 0,
	PAGE_TITLE: wgPageName,
	TALK_PAGE_TITLE: wgFormattedNamespaces[wgNamespaceNumber+1] + ":" + wgTitle,
	api: new WG.API(),
	SUMMARY_SUFFIX: "([[WP:WGG|WG]])",
	SUMMARY_MAX_LENGTH: 255
})
$.extend(WG, {
	load: function(){
		WG.api.pages.get(
			WG.PAGE_TITLE,
			function(markup, page){
				WG.token = page.edittoken
				WG.touched = page.touched
				WG.parseAndLoad(markup)
			},
			function(error){
				WG.console.error(error)
			}
		)
	},
	parseAndLoad: function(markup){
		WG.console.info("Parsing article content...")
		WG.chunks = new WG.Chunks((new WG.Chunker(markup)).popAll())
		
		//WG.console.info("Sending new markup of length " + WG.remarkuped.length + " to the API.")
		WG.api.pages.preview(
			WG.PAGE_TITLE,
			WG.chunks.remarkup(WG.SENTENCE_CLASS, WG.NOTE_CLASS, WG.HEADER_CLASS),
			function(html){
				WG.html = html
				$(document).ready(
					function(e){
						$("#bodyContent .mw-content-ltr").html(WG.html)
						/*$.contextMenu(
							{
								menu: WG.CONTEXT_MENU,
								selector: $("span." + WG.SENTENCE_CLASS),
								callback: function(action, el, pos){
									switch(action){
										case "edit":
											WG.loadSentenceInteractor(el)
											break;
										case "new_note":
											WG.loadNoteCreater(el)
											break;
									}
								}
							}
						)*/
						$("span." + WG.SENTENCE_CLASS)
							.hover(
								function(e){
									if(e.ctrlKey){
										$(e.currentTarget).addClass("hover")
									}
								},
								function(e){
									$(e.currentTarget).removeClass("hover")
								}
							)
							.click(
								function(e){
									WG.loadSentenceInteractor(e.currentTarget)
								}
							)
							
						
						/*$(
							"div." + WG.HEADER_CLASS + " h2,"+
							"div." + WG.HEADER_CLASS + " h3,"+
							"div." + WG.HEADER_CLASS + " h4,"+
							"div." + WG.HEADER_CLASS + " h5,"+
							"div." + WG.HEADER_CLASS + " h6,"+
							"div." + WG.HEADER_CLASS + " h7"
							)
							.append(
								$("<div/>")
									.addClass(WG.MARKUP_CLASS)
									.append("+ note")
							)*/
						
						var hash = window.location.hash.substring(1)
						WG.noteDrawer = new WG.NoteDrawer()
						$.each(
							$('span.' + WG.NOTE_CLASS),
							function(i, chunkSpan){
								var id = parseInt($(chunkSpan).attr('id').split("_")[1])
								var span = $(chunkSpan).children($('span.' + WG.SUB_NOTE_CLASS))
								var note = new WG.OldNote(
									WG.chunks.get(id, 'note'),
									span
								)
								WG.noteDrawer.add(note)
								if(note.id == hash){
									note.show()
								}
							}
						)
						WG.afterLoad()
					}
				)
			},
			function(error){
				WG.console.error(error)
			}
		)
	},
	loadSentenceInteractor: function(e){
		WG.lastSentenceInteractor = new WG.SentenceInteractor(e)
	},
	loadNoteCreater: function(e){
		var previousId = parseInt(e.attr('id').split("_")[1])
		d = new Date()
		
		var note = new WG.NewNote(previousId, WG.SUB_NOTE_CLASS)
		WG.lastNewNote = note
		note.span.insertAfter(e)
		WG.noteDrawer.add(note)
	},
	console: new WG.HiddenConsole(),
	error: function(message){
		if(confirm(message + "\nWould you like to reload the page?")){
			window.location.reload()
		}
	},
	afterLoad: function(){
		if(window.setupPopups){
			disablePopups()
			setupPopups()
		}
	}
})


WG.load()
// </syntaxhighlight>