/*!
* 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 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 == '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) != 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
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 or
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)')()
.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) {
// according to the specs accesskeys must be invoked immediately
opt.accesskeys.$node.trigger(opt.accesskeys.$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 || $this.is('.disabled, .context-menu-submenu, .context-menu-separator, .not-selectable')) {
return;
}
e.preventDefault();
e.stopImmediatePropagation();
if ($.isFunction(root.callbacks) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) {
// item-specific callback
callback = root.callbacks;
} 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.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.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 = undefined;
try {
delete opt;
} 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(, function(i,k){
opt = {};
if (!root) {
root = {};
}
});
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++) {
if (!root.accesskeys) {
root.accesskeys = 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) {
// run custom type handler
types.call($t, item, opt, root);
// register commands
$.each(, function(i,k){
k.commands = item;
if ($.isFunction(item.callback)) {
k.callbacks = 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(, function(i,k){
k.commands = item;
k.inputs = 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(, function(i,k){
k.commands = item;
if ($.isFunction(item.callback)) {
k.callbacks = 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,
disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true;
// dis- / enable item
$item('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++) {
k = k.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.ns;
}
menus = 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.$menu) {
menus.$menu.remove();
}
delete menus;
} catch(e) {
menus = 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) {
$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].$menu) {
menus].$menu.remove();
}
delete menus];
} catch(e) {
menus] = null;
}
$document.off(namespaces);
}
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').each(function() {
if (this.id) {
$.contextMenu({
selector: '',
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 || "";
break;
case 'checkbox':
item.selected = data ? true : false;
break;
case 'radio':
item.selected = (data || "") == item.value ? true : false;
break;
case 'select':
item.selected = data || "";
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 = item.$input.val();
break;
case 'checkbox':
data = item.$input.prop('checked');
break;
case 'radio':
if (item.$input.prop('checked')) {
data = item.value;
}
break;
}
});
return data;
};
// find <label for="xyz">
function inputLabel(node) {
return (node.id && $('label').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 >, <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).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 = 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 = typeof prop == "function" &&
typeof _super == "function" && fnTest.test(prop) ?
(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;
// 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) :
prop;
}
// 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
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(
'\\{\\{*' +
WG.NOTE_TEMPLATE.replace(/\:/g, "\\:").replace(/\//g, "\\/") + '*' +
'\\|?(*?)(\\|.*)*\\}\\}'
),
TOKEN_MAP: {
white_space: ' +',
number: "+(\\.+)?|(\\.+)",
word: '\\w+',
entity: '&\\w+;',
list_item: '\\n+(\\*|\\#|\\:)+',
def_item: '\\n+\\;',
header: '(\\n+|^)(=.+?=|==.+?==|===.+?===|====.+?====|=====.+?=====)',
open_table: '\\n*\\{\\||<table*>',
paragraph_break: '\\n{2,}',
open_note: '\\{\\{*' +
WG.NOTE_TEMPLATE.replace(/\:/g, "\\:").replace(/\//g, "\\/") +
"*\\|?",
open_image: '\\[\\[(Image|File):',
open_internal_link: '\\[\\[',
close_internal_link: '\\]\\]',
external_link: '\\\\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.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)
}
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
for(type in this.tokens){
var re = RegExp("^" + this.tokens + "$", "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(),
id: this.chunkId++
}
case "header":
return {
t: "header",
c: WG.concatTokens(),
id: this.chunkId++
}
case "paragraph_break":
return {
t: "paragraph_break",
c: WG.concatTokens(),
id: this.chunkId++
}
case "line_break":
return {
t: "break",
c: WG.concatTokens(),
id: this.chunkId++
}
case "ref":
return {
t: "ref",
c: WG.concatTokens(),
id: this.chunkId++
}
/*case "math":
return {
t: "math",
c: WG.concatTokens(),
id: this.chunkId++
}*/
case "pre":
return {
t: "pre",
c: WG.concatTokens(),
id: this.chunkId++
}
case "gallery":
return {
t: "gallery",
c: WG.concatTokens(),
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 =
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 =
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 =
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 =
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 =
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 =
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("_"))
this.chunk = WG.chunks.get(id)
this.currentHTML = this.span.html()
this.markup = {
ltrim: this.chunk.c.match(/^\s*/),
rtrim: this.chunk.c.match(/\s*$/),
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)
}
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
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
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
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.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
for(i in group.notes){var note = group.notes
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
//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.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.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
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
//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
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)
this.preamble = parts + match,
this.editor.val(parts.slice(1).join(match))
}else{
//No template :(. Try to process the first level two header.
var level2RE = /(^|\n)+==+?==/
match = markup.match(level2RE)
if(match){
var parts = markup.split(match)
this.preamble = parts + match,
this.editor.val(parts.slice(1).join(match))
}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("_"))
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 ]==\n" +
"{{subst:" + WG.NOTE_REFERENCE_INIT + "}}\n" +
this.editor.val(),
"Creating note page for ]",
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 ]",
function(note, callback){return function(save){
var summary = 'Inserting ] 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 ]",
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
}
if(page.revisions){
var markup = page.revisions
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){
throw "Chunk id " + id + " was not found."
}else if(type && this.chunks.t != type){
throw "Chunk id " + id + " is of type '" + this.chunks.t + "', not '" + type + "'"
}else{
return this.chunks
}
},
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
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
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.c
}
return newMarkup
},
remarkup: function(sentenceClass, noteClass, headerClass){
markup = ''
for(var i in this.chunks){
var chunk = this.chunks
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 + ":" + wgTitle,
api: new WG.API(),
SUMMARY_SUFFIX: "(])",
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("_"))
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("_"))
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>