User:DarTar/wg system.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:DarTar/wg system. |
/*!
* 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>